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 “' + task.text + '”</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) + " – " + 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>