Skip to main content
Back to examples
Popular features/Unscheduled tasks
Loading live demo…from dhtmlxcode.com
Popular features

Unscheduled tasks

Move a task between a backlog and the schedule in one click, edit its dates right in the grid, or drag its row between the Sprint and Backlog groups.

gantt.message({
	text: "Schedule or backlog a task in one click, edit dates right in the grid, or just drag the row between Sprint and Backlog.",
	expire: -1
});

gantt.config.date_format  = "%Y-%m-%d";
gantt.config.row_height    = 38;
gantt.config.bar_height    = 26;
gantt.config.scale_height  = 52;
gantt.config.inline_editors_date_processing = "keepDuration";

gantt.config.scales = [
	{ unit: "month", step: 1, format: "%F %Y" },
	{ unit: "week",  step: 1, format: "Week %W" }
];

let SPRINT_ID  = 10;
let BACKLOG_ID = 20;

/* Let rows be dragged freely between the two groups in the grid. */
gantt.config.order_branch = true;
gantt.config.order_branch_free = true;

gantt.attachEvent("onTaskLoading", function (task) {
	if (task.unscheduled == null && task.type !== gantt.config.types.project)
		task.unscheduled = !task.start_date;
	return true;
});

/* schedule / backlog transitions */
function nextAvailableDate() {
	let maxEnd = null;
	gantt.eachTask(function (t) {
		if (t.parent == SPRINT_ID && t.end_date && (!maxEnd || t.end_date > maxEnd)) maxEnd = t.end_date;
	});
	return maxEnd || gantt.date.day_start(new Date());
}

/* skipMove: true when the row was already dropped there by drag-and-drop. */
function scheduleTask(id, date, skipMove) {
	let task = gantt.getTask(id);
	task.start_date = gantt.getClosestWorkTime({ date, dir: "future", task });
	task.duration = task.type === gantt.config.types.milestone ? 0 : (task.duration || 3);
	task.end_date = gantt.calculateEndDate(task);
	task.unscheduled = false;
	gantt.updateTask(id);
	if (!skipMove && task.parent != SPRINT_ID)
		gantt.moveTask(id, gantt.getChildren(SPRINT_ID).length, SPRINT_ID);
	updateCount();
	gantt.showTask(id); // the new date can land outside the current viewport: always scroll to it
}
function backlogTask(id, skipMove) {
	let task = gantt.getTask(id);
	task.start_date = new Date(SPRINT_START);
	task.end_date = new Date(SPRINT_START);
	task.unscheduled = true;
	gantt.updateTask(id);
	if (!skipMove && task.parent != BACKLOG_ID)
		gantt.moveTask(id, gantt.getChildren(BACKLOG_ID).length, BACKLOG_ID);
	updateCount();
}

/* Dragging a row between the Sprint / Backlog groups mirrors the button. */
gantt.attachEvent("onBeforeTaskMove", function (id, parent) {
	let task = gantt.getTask(id);
	if (task.type === gantt.config.types.project) return true;
	return parent == SPRINT_ID || parent == BACKLOG_ID;
});
gantt.attachEvent("onAfterTaskMove", function (id, parent) {
	let task = gantt.getTask(id);
	if (task.type === gantt.config.types.project) return;
	if (parent == SPRINT_ID && task.unscheduled) scheduleTask(id, nextAvailableDate(), true);
	else if (parent == BACKLOG_ID && !task.unscheduled) backlogTask(id, true);
});

/* schedule popup */
let activePopup = null;
function toISO(d) {
	let m = d.getMonth() + 1, day = d.getDate();
	return d.getFullYear() + "-" + (m < 10 ? "0" : "") + m + "-" + (day < 10 ? "0" : "") + day;
}
function closePopup() {
	if (!activePopup) return;
	activePopup.parentNode.removeChild(activePopup);
	activePopup = null;
	document.removeEventListener("mousedown", onDocClick, true);
}
function onDocClick(e) {
	if (activePopup && !activePopup.contains(e.target)) closePopup();
}
function openSchedulePopup(event, taskId) {
	event.stopPropagation();
	closePopup();

	const task = gantt.getTask(taskId);
	const defaultScheduleDate = nextAvailableDate();
	const scheduleButton = event.currentTarget;
	const scheduleButtonRectangle = scheduleButton.getBoundingClientRect();

	const schedulePopup = document.createElement("div");
	schedulePopup.className = "sched_popup";
	schedulePopup.innerHTML =
		'<div class="sched_popup__title">Schedule &ldquo;' + task.text + '&rdquo;</div>' +
		'<div class="sched_popup__row">' +
			'<button type="button" class="sched_popup__chip" data-preset="next">Next slot</button>' +
			'<button type="button" class="sched_popup__chip" data-preset="today">Today</button>' +
			'<button type="button" class="sched_popup__chip" data-preset="week">+1 week</button>' +
		'</div>' +
		'<div class="sched_popup__row" style="margin-bottom:0">' +
			'<input type="date" class="sched_popup__date" value="' +
				toISO(defaultScheduleDate) +
			'">' +
		'</div>' +
		'<div class="sched_popup__foot">' +
			'<button type="button" class="dhx_btn" data-act="cancel">Cancel</button>' +
			'<button type="button" class="dhx_btn dhx_btn--primary" data-act="confirm">Schedule</button>' +
		'</div>';

	document.body.appendChild(schedulePopup);

	const viewportPadding = 8;
	const popupGap = 6;

	const maximumPopupLeft = window.innerWidth - schedulePopup.offsetWidth - viewportPadding;

	const popupLeft = Math.min(
		Math.max(viewportPadding, scheduleButtonRectangle.left),
		Math.max(viewportPadding, maximumPopupLeft)
	);

	const availableSpaceBelow = window.innerHeight - scheduleButtonRectangle.bottom - popupGap - viewportPadding;
	const availableSpaceAbove = scheduleButtonRectangle.top - popupGap - viewportPadding;

	let popupTop;
	if (availableSpaceBelow >= schedulePopup.offsetHeight || availableSpaceBelow >= availableSpaceAbove) {
		popupTop = scheduleButtonRectangle.bottom + popupGap;
	} else {
		popupTop = scheduleButtonRectangle.top - schedulePopup.offsetHeight - popupGap;
	}

	const maximumPopupTop = window.innerHeight - schedulePopup.offsetHeight - viewportPadding;

	popupTop = Math.min(Math.max(viewportPadding, popupTop), Math.max(viewportPadding, maximumPopupTop));

	schedulePopup.style.left = popupLeft + "px";
	schedulePopup.style.top = popupTop + "px";

	const dateInput = schedulePopup.querySelector(".sched_popup__date");


	schedulePopup.querySelectorAll("[data-preset]").forEach(function (presetButton) {
		presetButton.onclick = function () {
			const selectedPreset = presetButton.getAttribute("data-preset");

			let selectedDate;
			if (selectedPreset === "today") {
				selectedDate = new Date();
			} else if (selectedPreset === "week") {
				selectedDate = gantt.date.add(new Date(), 7, "day");
			} else {
				selectedDate = nextAvailableDate();
			}

			dateInput.value = toISO(selectedDate);
		};
	});

	schedulePopup.querySelector('[data-act="cancel"]').onclick = closePopup;

	schedulePopup.querySelector('[data-act="confirm"]').onclick = function () {
		const selectedDate = dateInput.value ? new Date(dateInput.value) : defaultScheduleDate;

		scheduleTask(taskId, selectedDate, false);
		closePopup();
	};

	activePopup = schedulePopup;

	setTimeout(function () {
		document.addEventListener("mousedown", onDocClick, true);
	}, 0);
}

/* grid */
function statusCell(task) {
	if (task.type === gantt.config.types.project) return "";
	return task.unscheduled
		? '<span class="chip chip--backlog">Backlog</span>'
		: '<span class="chip chip--sched">Scheduled</span>';
}
function dateCell(task, field) {
	if (task.type === gantt.config.types.project) return "";
	if (task.unscheduled || !task[field]) return '<span class="cell_placeholder">Set date</span>';
	return gantt.templates.date_grid(task[field], task);
}
let ICON = {
	sched:   '<svg viewBox="0 0 16 16"><rect x="2.5" y="3" width="11" height="10.5" rx="1.6"/><path d="M2.5 6.3h11"/><path d="M5.4 2v2.4M10.6 2v2.4"/></svg>',
	backlog: '<svg viewBox="0 0 16 16"><path d="M12.5 8h-9"/><path d="M6.3 4.3 3.5 8l2.8 3.7"/></svg>'
};
function actionCell(task) {
	if (task.type === gantt.config.types.project) return "";
	return task.unscheduled
		? '<span class="row_act row_act--sched" onclick="openSchedulePopup(event,' + task.id + ')">' + ICON.sched + "Schedule</span>"
		: '<span class="row_act row_act--backlog" onclick="event.stopPropagation();backlogTask(' + task.id + ')">' + ICON.backlog + "To backlog</span>";
}

gantt.config.columns = [
	{ name: "text",       label: "Item",   tree: true, width: 230, resize: true },
	{ name: "status",     label: "Status", align: "center", width: 96, template: statusCell, resize: true },
	{ name: "start_date", label: "Start",  align: "center", width: 100, resize: true,
		template: function (task) { return dateCell(task, "start_date"); },
		editor: { type: "date", map_to: "start_date" } },
	{ name: "end_date",   label: "End",    align: "center", width: 100, resize: true,
		template: function (task) { return dateCell(task, "end_date"); },
		editor: { type: "date", map_to: "end_date" } },
	{ name: "action",     label: "",       align: "center", width: 118, resize: true, template: actionCell },
	{ name: "add",        width: 40, resize: true }
];

/* Flip unscheduled -> scheduled the moment a date is typed into the grid. */
gantt.ext.inlineEditors.attachEvent("onSave", function (state) {
	if (state.columnName !== "start_date" && state.columnName !== "end_date") return;
	let task = gantt.getTask(state.id);
	if (task.unscheduled && task.start_date && task.end_date) {
		task.unscheduled = false;
		gantt.updateTask(state.id);
		scheduleTask(state.id, state.newValue);
		updateCount();
	}
	// if the date is deleted, move to backlog
	if (+state.newValue <= 946580400000){
		backlogTask(task.id)
	}
});

/* Dashed placeholder for undated bars, plus a small right-side flag. */
gantt.templates.task_class = function (start, end, task) {
	let cls = task.unscheduled ? "is_unscheduled" : "";
	if (task.$justMoved) cls += " just_moved";
	return cls;
};
gantt.templates.rightside_text = function (start, end, task) {
	if (task.type === gantt.config.types.milestone) return task.text;
	if (task.unscheduled) return '<span class="rside_flag">no dates yet</span>';
	return "";
};
gantt.templates.tooltip_text = function (start, end, task) {
	if (task.unscheduled) return "<b>" + task.text + "</b><br>In backlog: not scheduled";
	return "<b>" + task.text + "</b><br>" +
		gantt.templates.tooltip_date_format(start) + " &ndash; " + gantt.templates.tooltip_date_format(end);
};

/* filter */
let filterMode = "all";
gantt.attachEvent("onBeforeTaskDisplay", function (id, task) {
	if (filterMode === "all") return true;
	if (task.type === gantt.config.types.project) return true;
	return filterMode === "backlog" ? !!task.unscheduled : !task.unscheduled;
});
function setFilter(mode) {
	filterMode = mode;
	DHX.ui.setActiveByAttribute("#filter_seg .dhx_btn", "data-mode", mode, "active");
	gantt.render();
}

function updateCount() {
	let n = 0;
	gantt.eachTask(function (t) { if (t.unscheduled) n++; });
	DHX.ui.setText("unsched_count", n);
}
gantt.attachEvent("onAfterTaskAdd", updateCount);
gantt.attachEvent("onAfterTaskDelete", updateCount);

/* data */
/* Anchored to today (not a fixed year) so the sample never looks stale and the
   "Today" / "Next slot" presets always land inside the visible sprint window. */
function addDays(base, n) {
	let d = gantt.date.day_start(new Date(base));
	return gantt.date.add(d, n, "day");
}
let TODAY = gantt.date.day_start(new Date());
let SPRINT_START = addDays(TODAY, -7);

let data = {
	tasks: [
		{ id: 1, text: "Payments 2.0", type: "project", open: true, progress: 0.4 },

		{ id: SPRINT_ID, text: "Sprint 24 (current)", type: "project", parent: 1, open: true },
		{ id: 11, text: "Apple Pay integration",  parent: SPRINT_ID, start_date: addDays(SPRINT_START, 0),  duration: 6, progress: 1 },
		{ id: 12, text: "Refund API",             parent: SPRINT_ID, start_date: addDays(SPRINT_START, 8),  duration: 5, progress: 0.7 },
		{ id: 13, text: "3-D Secure step-up",     parent: SPRINT_ID, start_date: addDays(SPRINT_START, 15), duration: 7, progress: 0.3 },
		{ id: 14, text: "Chargeback dashboard",   parent: SPRINT_ID, start_date: addDays(SPRINT_START, 21), duration: 5, progress: 0.1 },
		{ id: 16, text: "Payout ledger sync",     parent: SPRINT_ID, start_date: addDays(SPRINT_START, 2),  duration: 4, progress: 0.9 },
		{ id: 17, text: "Webhook retry queue",    parent: SPRINT_ID, start_date: addDays(SPRINT_START, 10), duration: 4, progress: 0.5 },
		{ id: 18, text: "PCI scan remediation",   parent: SPRINT_ID, start_date: addDays(SPRINT_START, 17), duration: 3, progress: 0.2 },
		{ id: 19, text: "Invoice PDF export",     parent: SPRINT_ID, start_date: addDays(SPRINT_START, 24), duration: 4, progress: 0 },
		{ id: 15, text: "Sprint 24 review",       parent: SPRINT_ID, type: "milestone", start_date: addDays(SPRINT_START, 29) },

		{ id: BACKLOG_ID, text: "Backlog", type: "project", parent: 1, open: true },
		{ id: 21, text: "Saved cards & wallets",  parent: BACKLOG_ID, unscheduled: true, duration: 4, progress: 0 },
		{ id: 22, text: "Multi-currency pricing", parent: BACKLOG_ID, unscheduled: true, duration: 5, progress: 0 },
		{ id: 23, text: "Subscription retries",   parent: BACKLOG_ID, unscheduled: true, duration: 3, progress: 0 },
		{ id: 24, text: "Fraud scoring v2",       parent: BACKLOG_ID, unscheduled: true, duration: 6, progress: 0 },
		{ id: 25, text: "Dispute workflow",       parent: BACKLOG_ID, unscheduled: true, duration: 4, progress: 0 },
		{ id: 26, text: "Bank transfer payout",   parent: BACKLOG_ID, unscheduled: true, duration: 3, progress: 0 },
		{ id: 28, text: "Recurring billing v2",   parent: BACKLOG_ID, unscheduled: true, duration: 5, progress: 0 },
		{ id: 29, text: "Tax calculation engine", parent: BACKLOG_ID, unscheduled: true, duration: 6, progress: 0 },
		{ id: 30, text: "Risk rules editor UI",   parent: BACKLOG_ID, unscheduled: true, duration: 4, progress: 0 },
		{ id: 27, text: "Public API launch",      parent: BACKLOG_ID, unscheduled: true, type: "milestone" }
	],
	links: [
		{ id: 1, source: 11, target: 12, type: "0" },
		{ id: 2, source: 12, target: 13, type: "0" },
		{ id: 3, source: 13, target: 14, type: "0" },
		{ id: 4, source: 14, target: 15, type: "0" },
		{ id: 5, source: 16, target: 17, type: "0" },
		{ id: 6, source: 17, target: 18, type: "0" },
		{ id: 7, source: 18, target: 19, type: "0" }
	]
};

gantt.init("gantt_here");
gantt.parse(data);
updateCount();
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title>Unscheduled tasks v2: one-click schedule | 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; }
		#gantt_here { flex: 1 1 auto; min-height: 0; width: 100%; }

		.gantt_task_line { border: none; border-radius: 7px; box-shadow: 0 1px 2px rgba(16,24,40,.18); }
		.gantt_task_line .gantt_task_progress { border-radius: 7px 0 0 7px; }
		.gantt_task_line.gantt_project { border-radius: 5px; }

		.gantt_task_line.is_unscheduled {
			background: repeating-linear-gradient(45deg,#cbd5e1,#cbd5e1 6px,#e2e8f0 6px,#e2e8f0 12px);
			box-shadow: none;
			border: 1px dashed #94a3b8;
		}
		.gantt_task_line.is_unscheduled .gantt_task_progress { display: none; }

		.chip {
			display: inline-block;
			padding: 2px 9px;
			border-radius: 999px;
			font: 600 11px var(--dhx-font);
			letter-spacing: .2px;
		}
		.chip--sched   { background: var(--dhx-ok-bg); color: var(--dhx-ok-ink); }
		.chip--backlog { background: var(--dhx-neutral-bg); color: var(--dhx-neutral-ink); }

		.rside_flag {
			font: 600 11px var(--dhx-font);
			color: #94a3b8;
			padding-left: 2px;
		}

		.hint { font: 500 12px var(--dhx-font); color: var(--dhx-muted); }
		.hint b { color: var(--dhx-ink-2); font-weight: 600; }
		.count_pill {
			display: inline-flex; align-items: center; gap: 6px;
			padding: 4px 11px; border-radius: 999px;
			background: var(--dhx-warn-bg); color: var(--dhx-warn-ink);
			font: 600 12px var(--dhx-font);
			border: 1px solid var(--dhx-warn-line);
		}
		.count_pill .dot { width: 7px; height: 7px; border-radius: 50%; background: #f59e0b; }

		.gantt_cell[data-column-name="start_date"],
		.gantt_cell[data-column-name="end_date"] { cursor: text; }
		.cell_placeholder { color: var(--dhx-muted); font-style: italic; }

		.row_act {
			display: inline-flex; align-items: center; gap: 5px;
			height: 25px; padding: 0 10px;
			border-radius: 999px;
			border: 1px solid transparent;
			font: 600 11px var(--dhx-font);
			cursor: pointer;
			transition: background .12s, border-color .12s, color .12s;
			white-space: nowrap;
		}
		.row_act svg { width: 12px; height: 12px; stroke: currentColor; stroke-width: 2; fill: none; stroke-linecap: round; stroke-linejoin: round; flex: none; }
		.row_act--sched { background: var(--dhx-info-bg); color: var(--dhx-info-ink); border-color: var(--dhx-info-line); }
		.row_act--sched:hover { background: #dbe8fd; }
		.row_act--backlog { background: #f2f4f7; color: var(--dhx-ink-2); }
		.row_act--backlog:hover { background: #e7eaee; color: var(--dhx-ink); }

		:root[data-gantt-theme="dark"] .row_act--sched:hover   { background: #25324a; }
		:root[data-gantt-theme="dark"] .row_act--backlog   { background: #323a45; color: var(--dhx-ink); }
		:root[data-gantt-theme="dark"] .row_act--backlog:hover { background: #3c4550; }

		.gantt_row[task_id] .gantt_tree_content { cursor: grab; }

		@keyframes row_flash { 0% { box-shadow: 0 0 0 6px rgba(37,99,235,.45); } 100% { box-shadow: 0 0 0 0 rgba(37,99,235,0); } }
		.gantt_task_line.just_moved { animation: row_flash .9s ease-out; }

		.sched_popup {
			position: fixed;
			z-index: 50;
			width: 232px;
			box-sizing: border-box;
			max-height: calc(100vh - 16px);
			overflow-y: auto;
			background: var(--dhx-overlay);
			border: 1px solid var(--dhx-line);
			border-radius: 10px;
			box-shadow: 0 10px 30px rgba(16,24,40,.20);
			padding: 12px;
		}
		.sched_popup__title {
			font: 600 12.5px var(--dhx-font); color: var(--dhx-ink);
			margin-bottom: 9px;
			white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
		}
		.sched_popup__row { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
		.sched_popup__chip {
			height: 25px; padding: 0 9px; border-radius: 999px;
			border: 1px solid var(--dhx-line); background: var(--dhx-surface); color: var(--dhx-ink-2);
			font: 600 11px var(--dhx-font); cursor: pointer;
		}
		.sched_popup__chip:hover { border-color: var(--dhx-accent); color: var(--dhx-accent); }
		:root[data-gantt-theme="dark"] .sched_popup { box-shadow: 0 10px 30px rgba(0,0,0,.5); }
		.sched_popup__date {
			width: 100%; height: 32px; padding: 0 8px; box-sizing: border-box;
			border: 1px solid var(--dhx-line); border-radius: 7px;
			font: 500 13px var(--dhx-font); color: var(--dhx-ink);
		}
		.sched_popup__foot { display: flex; justify-content: flex-end; gap: 6px; margin-top: 10px; }
	</style>
</head>
<body>

<div class="dhx_header" style="justify-content:center; gap:14px">
	<span class="hint">Show</span>
	<div class="dhx_seg" id="filter_seg">
		<button class="dhx_btn active" data-mode="all"      onclick="setFilter('all')">All</button>
		<button class="dhx_btn"        data-mode="sched"    onclick="setFilter('sched')">Scheduled</button>
		<button class="dhx_btn"        data-mode="backlog"  onclick="setFilter('backlog')">Backlog</button>
	</div>
	<span class="count_pill"><span class="dot"></span><span id="unsched_count">0</span> unscheduled</span>
</div>

<div id="gantt_here"></div>


</body>
</html>