Professional demos/Enterprise Gantt demo
Loading live demo…from dhtmlxcode.com
Professional demos
Enterprise Gantt demo
A comprehensive Gantt chart demo featuring tasks, dependencies, resource management, and interactive controls.
gantt.plugins({
drag_timeline: true,
marker: true,
undo: true,
tooltip: true,
critical_path: true,
fullscreen: true,
grouping: true,
auto_scheduling: true,
export_api: true
});
gantt.config.date_format = "%Y-%m-%d";
gantt.config.date_grid = "%d %M";
gantt.config.auto_types = true;
gantt.config.grid_resize = true;
gantt.config.open_tree_initially = true;
gantt.config.order_branch = "marker";
gantt.config.order_branch_free = true;
gantt.config.resources = true;
gantt.config.resource_property = "resources";
gantt.config.fit_tasks = true;
gantt.config.grid_elastic_columns = true;
gantt.config.scroll_size = 16;
gantt.config.reorder_grid_columns = true;
/* baseline + slack + auto-scheduling: all OFF by default, toggled from UI */
gantt.config.baselines.render_mode = "separateRow";
gantt.config.resize_rows = true;
gantt.config.min_task_grid_row_height = 10;
gantt.config.auto_scheduling = { enabled: false, apply_constraints: true, show_constraints: true, gap_behavior: "preserve" };
let showSlack = false;
/* ---------------------------------------------------------------------------
* RESOURCES (work / material / cost), grouped by type
* ------------------------------------------------------------------------ */
const avatarColors = ["#03A9F4", "#f57730", "#e157de", "#78909C", "#8D6E63",
"#43A047", "#5E35B1", "#00897B", "#C0392B", "#7E57C2"];
const resourcesData = [
// group nodes
{ id: "g_work", text: "Work", type: "group" },
{ id: "g_material", text: "Material", type: "group" },
{ id: "g_cost", text: "Cost", type: "group" },
// work resources
{ id: 1, text: "Priya Nair", type: "work", parent: "g_work", unit: "h", max_units: 100, accrue: "Prorated", cost: 620, calendar: "", city: "Austin", avatar: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_23.jpg" },
{ id: 2, text: "Owen Bennett", type: "work", parent: "g_work", unit: "h", max_units: 100, accrue: "Prorated", cost: 580, calendar: "", city: "Dublin", avatar: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_02.jpg" },
{ id: 3, text: "Noah Kimura", type: "work", parent: "g_work", unit: "h", max_units: 100, accrue: "Prorated", cost: 640, calendar: "Business hours", city: "Toronto", avatar: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_03.jpg" },
{ id: 4, text: "Farah Haddad", type: "work", parent: "g_work", unit: "h", max_units: 100, accrue: "Prorated", cost: 600, calendar: "Night shift", city: "Singapore", avatar: "https://snippet.dhtmlx.com/codebase/data/common/img/02/avatar_04.jpg" },
{ id: 5, text: "Lukas Weber", type: "work", parent: "g_work", unit: "h", max_units: 100, accrue: "Prorated", cost: 610, calendar: "Night shift", city: "Berlin" },
{ id: 6, text: "Maya Torres", type: "work", parent: "g_work", unit: "h", max_units: 100, accrue: "Prorated", cost: 590, calendar: "", city: "Austin" },
{ id: 7, text: "Ines Duarte", type: "work", parent: "g_work", unit: "h", max_units: 100, accrue: "Prorated", cost: 560, calendar: "", city: "Madrid" },
{ id: 8, text: "Callum Reid", type: "work", parent: "g_work", unit: "h", max_units: 100, accrue: "Prorated", cost: 630, calendar: "", city: "Toronto" },
// material resource
{ id: 20, text: "Field laptop", type: "material", parent: "g_material", material_label: "laptop(s)", max_units: 100, accrue: "Prorated", cost: 1800, calendar: "", city: "Dublin" },
// cost resource
{ id: 30, text: "Cloud infrastructure", type: "cost", parent: "g_cost", max_units: 100, accrue: "Prorated", cost: 1200, calendar: "", city: "" }
];
// quick lookup + colors / initials for avatar chips
const resourceById = {};
resourcesData.forEach((r) => {
resourceById[r.id] = r;
if (r.type !== "group") {
r.initials = r.text.split(/\s+/).map((word) => word[0]).join("").substring(0, 2).toUpperCase();
r.color = avatarColors[(r.id) % avatarColors.length];
}
});
function avatarHtml(r) {
if (r.avatar) {
return `<span class="avatar" style="background:url(${r.avatar}) center/cover,${r.color}" title="${r.text}"></span>`;
}
return `<span class="avatar" style="background:${r.color}" title="${r.text}">${r.initials}</span>`;
}
// options list for the lightbox "resources" control (key = resource id)
const resourceOptions = resourcesData.filter((r) => r.type !== "group").map((r) => {
return { key: r.id, label: r.text, unit: r.unit || r.material_label || "units" };
});
/* ---------------------------------------------------------------------------
* TASK DATASETS
* ------------------------------------------------------------------------ */
const projects = {
saas: {
label: "AI Rollout",
title: "Enterprise AI Rollout",
logo: "AI",
logoColor: "linear-gradient(140deg,#0ea5e9,#2563eb)",
status: "On track",
statusColor: "#16a34a",
marker_start: "2026-07-06",
marker_important: "2026-07-20",
data: {
tasks: [
{ id: 1, text: "Enterprise AI Rollout", type: "project", progress: 0.46, open: true },
{ id: 2, text: "Business Case & Governance", type: "project", parent: 1, progress: 0.82, open: true },
{ id: 3, text: "Define use cases and ROI model", start_date: "2026-07-06", duration: 4, parent: 2, progress: 1, cost: 4800, resources: [{ resource_id: 1, value: 8 }, { resource_id: 7, value: 8 }] },
{ id: 4, text: "Security and legal risk review", start_date: "2026-07-10", duration: 5, parent: 2, progress: 0.85, cost: 7200, resources: [{ resource_id: 3, value: 8 }, { resource_id: 8, value: 8 }], deadline: "2026-07-16" },
{ id: 5, text: "Vendor shortlisting", start_date: "2026-07-10", duration: 4, parent: 2, progress: 0.9, cost: 5000, resources: [{ resource_id: 1, value: 8 }, { resource_id: 2, value: 8 }] },
{ id: 6, text: "Steering committee approval", start_date: "2026-07-15", type: "milestone", parent: 2, progress: 1 },
{ id: 7, text: "Data Foundation", type: "project", parent: 1, progress: 0.55, open: true },
{ id: 8, text: "Map customer support data sources", start_date: "2026-07-15", duration: 5, parent: 7, progress: 0.8, cost: 6500, resources: [{ resource_id: 1, value: 8 }, { resource_id: 6, value: 8 }] },
{ id: 9, text: "Data quality remediation sprint", start_date: "2026-07-20", duration: 8, parent: 7, progress: 0.42, cost: 12000, resources: [{ resource_id: 4, value: 8 }, { resource_id: 5, value: 8 }], deadline: "2026-07-28" },
{ id: 10, text: "Build anonymized training dataset", start_date: "2026-07-28", duration: 6, parent: 7, progress: 0.25, cost: 9000, resources: [{ resource_id: 5, value: 8 }, { resource_id: 20, value: 1 }] },
{ id: 11, text: "Data readiness gate", start_date: "2026-08-03", type: "milestone", parent: 7, progress: 0 },
{ id: 12, text: "Platform Integration", type: "project", parent: 1, progress: 0.31, open: true },
{ id: 13, text: "Configure AI gateway and audit logging", start_date: "2026-07-15", duration: 7, parent: 12, progress: 0.55, cost: 15000, resources: [{ resource_id: 4, value: 8 }, { resource_id: 30, value: 1 }], deadline: "2026-07-24" },
{ id: 14, text: "Connect CRM and ticketing workflows", start_date: "2026-08-03", duration: 8, parent: 12, progress: 0.18, cost: 18000, resources: [{ resource_id: 5, value: 8 }, { resource_id: 6, value: 8 }] },
{ id: 15, text: "Build agent handoff rules", start_date: "2026-08-11", duration: 5, parent: 12, progress: 0, cost: 7000, resources: [{ resource_id: 4, value: 8 }, { resource_id: 7, value: 8 }] },
{ id: 16, text: "Performance and failover testing", start_date: "2026-08-16", duration: 5, parent: 12, progress: 0, cost: 6000, resources: [{ resource_id: 3, value: 8 }], deadline: "2026-08-20" },
{ id: 17, text: "Adoption & Launch", type: "project", parent: 1, progress: 0.11, open: true },
{ id: 18, text: "Train support leads", start_date: "2026-08-03", duration: 4, parent: 17, progress: 0.15, cost: 4000, resources: [{ resource_id: 7, value: 8 }, { resource_id: 8, value: 8 }] },
{ id: 19, text: "Pilot with enterprise accounts", start_date: "2026-08-21", duration: 10, parent: 17, progress: 0, cost: 10000, resources: [{ resource_id: 7, value: 8 }, { resource_id: 8, value: 8 }], deadline: "2026-09-01" },
{ id: 20, text: "KPI review and rollback decision", start_date: "2026-08-31", type: "milestone", parent: 17, progress: 0 },
{ id: 21, text: "Production rollout", start_date: "2026-08-31", duration: 4, parent: 17, progress: 0, cost: 7000, resources: [{ resource_id: 4, value: 8 }, { resource_id: 6, value: 8 }] },
{ id: 22, text: "Post-launch optimization", start_date: "2026-09-04", duration: 5, parent: 17, progress: 0, cost: 5000, resources: [{ resource_id: 1, value: 8 }, { resource_id: 5, value: 8 }] }
],
links: [
{ id: 1, source: 3, target: 4, type: "0" },
{ id: 2, source: 3, target: 5, type: "0" },
{ id: 3, source: 4, target: 6, type: "0" },
{ id: 4, source: 5, target: 6, type: "0" },
{ id: 5, source: 6, target: 8, type: "0" },
{ id: 6, source: 8, target: 9, type: "0" },
{ id: 7, source: 9, target: 10, type: "0" },
{ id: 8, source: 10, target: 11, type: "0" },
{ id: 9, source: 11, target: 14, type: "0" },
{ id: 10, source: 13, target: 14, type: "0" },
{ id: 11, source: 14, target: 15, type: "0" },
{ id: 12, source: 15, target: 16, type: "0" },
{ id: 13, source: 11, target: 18, type: "0" },
{ id: 14, source: 18, target: 19, type: "0" },
{ id: 15, source: 16, target: 19, type: "0" },
{ id: 16, source: 19, target: 20, type: "0" },
{ id: 17, source: 20, target: 21, type: "0" },
{ id: 18, source: 21, target: 22, type: "0" },
{ id: 19, source: 6, target: 13, type: "0" }
],
baselines: [
{ id: 1, task_id: 3, start_date: "2026-07-06", end_date: "2026-07-09" },
{ id: 2, task_id: 4, start_date: "2026-07-10", end_date: "2026-07-16" },
{ id: 3, task_id: 9, start_date: "2026-07-17", end_date: "2026-07-27" },
{ id: 4, task_id: 10, start_date: "2026-07-26", end_date: "2026-08-02" },
{ id: 5, task_id: 13, start_date: "2026-07-15", end_date: "2026-07-23" },
{ id: 6, task_id: 14, start_date: "2026-08-01", end_date: "2026-08-10" },
{ id: 7, task_id: 18, start_date: "2026-07-31", end_date: "2026-08-05" },
{ id: 8, task_id: 19, start_date: "2026-08-18", end_date: "2026-08-31" },
{ id: 9, task_id: 21, start_date: "2026-08-30", end_date: "2026-09-02" }
]
}
},
mobile: {
label: "Fulfillment",
title: "Retail Fulfillment Network Upgrade",
logo: "FN",
logoColor: "linear-gradient(140deg,#f97316,#dc2626)",
status: "At risk",
statusColor: "#f59e0b",
marker_start: "2026-06-29",
marker_important: "2026-07-13",
data: {
tasks: [
{ id: 1, text: "Retail Fulfillment Network Upgrade", type: "project", progress: 0.38, open: true },
{ id: 2, text: "Program Control", type: "project", parent: 1, progress: 0.64, open: true },
{ id: 3, text: "Lock peak-season business scope", start_date: "2026-06-29", duration: 4, parent: 2, progress: 1, cost: 4200, resources: [{ resource_id: 1, value: 8 }, { resource_id: 7, value: 8 }], deadline: "2026-07-03" },
{ id: 4, text: "Align store, ecommerce, logistics KPIs", start_date: "2026-07-03", duration: 3, parent: 2, progress: 0.82, cost: 3600, resources: [{ resource_id: 1, value: 8 }, { resource_id: 8, value: 8 }] },
{ id: 5, text: "Executive go/no-go for pilot", start_date: "2026-07-08", type: "milestone", parent: 2, progress: 1 },
{ id: 6, text: "Warehouse Automation", type: "project", parent: 1, progress: 0.42, open: true },
{ id: 7, text: "Conveyor controls audit", start_date: "2026-07-09", duration: 4, parent: 6, progress: 0.75, cost: 8500, resources: [{ resource_id: 3, value: 8 }, { resource_id: 30, value: 1 }] },
{ id: 8, text: "Robotics vendor contract", start_date: "2026-07-09", duration: 6, parent: 6, progress: 0.48, cost: 24000, resources: [{ resource_id: 2, value: 8 }, { resource_id: 3, value: 8 }], deadline: "2026-07-17" },
{ id: 9, text: "Install pilot picking zone", start_date: "2026-07-20", duration: 9, parent: 6, progress: 0.22, cost: 58000, resources: [{ resource_id: 5, value: 8 }, { resource_id: 30, value: 1 }], deadline: "2026-07-31" },
{ id: 10, text: "Safety certification", start_date: "2026-08-03", duration: 4, parent: 6, progress: 0, cost: 7000, resources: [{ resource_id: 3, value: 8 }, { resource_id: 8, value: 8 }], deadline: "2026-08-07" },
{ id: 11, text: "ERP & Order Routing", type: "project", parent: 1, progress: 0.32, open: true },
{ id: 12, text: "Inventory API mapping", start_date: "2026-07-09", duration: 6, parent: 11, progress: 0.72, cost: 9000, resources: [{ resource_id: 4, value: 8 }, { resource_id: 6, value: 8 }] },
{ id: 13, text: "Build allocation rules for online orders", start_date: "2026-07-17", duration: 7, parent: 11, progress: 0.4, cost: 11000, resources: [{ resource_id: 1, value: 8 }, { resource_id: 4, value: 8 }] },
{ id: 14, text: "Integrate carrier rate shopping", start_date: "2026-07-28", duration: 5, parent: 11, progress: 0.14, cost: 8000, resources: [{ resource_id: 5, value: 8 }, { resource_id: 6, value: 8 }] },
{ id: 15, text: "Exception dashboard for delayed shipments", start_date: "2026-08-04", duration: 5, parent: 11, progress: 0, cost: 7000, resources: [{ resource_id: 6, value: 8 }, { resource_id: 20, value: 1 }] },
{ id: 16, text: "Store Operations Readiness", type: "project", parent: 1, progress: 0.26, open: true },
{ id: 17, text: "Train regional operations leads", start_date: "2026-08-10", duration: 4, parent: 16, progress: 0, cost: 5200, resources: [{ resource_id: 7, value: 8 }, { resource_id: 8, value: 8 }] },
{ id: 18, text: "Run store pickup simulation", start_date: "2026-08-17", duration: 5, parent: 16, progress: 0, cost: 6400, resources: [{ resource_id: 1, value: 8 }, { resource_id: 7, value: 8 }], deadline: "2026-08-21" },
{ id: 19, text: "Labor planning sign-off", start_date: "2026-08-24", type: "milestone", parent: 16, progress: 0 },
{ id: 20, text: "Pilot Launch", type: "project", parent: 1, progress: 0.05, open: true },
{ id: 21, text: "Freeze pilot scope", start_date: "2026-08-25", type: "milestone", parent: 20, progress: 0 },
{ id: 22, text: "Pilot launch in Chicago DC", start_date: "2026-08-26", duration: 3, parent: 20, progress: 0, cost: 16000, resources: [{ resource_id: 3, value: 8 }, { resource_id: 4, value: 8 }, { resource_id: 30, value: 1 }], deadline: "2026-08-28" },
{ id: 23, text: "Measure order-cycle-time reduction", start_date: "2026-08-31", duration: 5, parent: 20, progress: 0, cost: 4200, resources: [{ resource_id: 1, value: 8 }, { resource_id: 7, value: 8 }] },
{ id: 24, text: "National rollout recommendation", start_date: "2026-09-08", type: "milestone", parent: 20, progress: 0 }
],
links: [
{ id: 1, source: 3, target: 4, type: "0" },
{ id: 2, source: 4, target: 5, type: "0" },
{ id: 3, source: 5, target: 7, type: "0" },
{ id: 4, source: 5, target: 8, type: "0" },
{ id: 5, source: 7, target: 9, type: "0" },
{ id: 6, source: 8, target: 9, type: "0", lag: 2 },
{ id: 7, source: 9, target: 10, type: "0" },
{ id: 8, source: 5, target: 12, type: "0" },
{ id: 9, source: 12, target: 13, type: "0" },
{ id: 10, source: 13, target: 14, type: "0" },
{ id: 11, source: 14, target: 15, type: "0" },
{ id: 12, source: 10, target: 18, type: "0" },
{ id: 13, source: 15, target: 18, type: "0" },
{ id: 14, source: 17, target: 18, type: "0" },
{ id: 15, source: 18, target: 19, type: "0" },
{ id: 16, source: 19, target: 21, type: "0" },
{ id: 17, source: 10, target: 21, type: "0" },
{ id: 18, source: 15, target: 21, type: "0" },
{ id: 19, source: 21, target: 22, type: "0" },
{ id: 20, source: 22, target: 23, type: "0" },
{ id: 21, source: 23, target: 24, type: "0" }
],
baselines: [
{ id: 1, task_id: 3, start_date: "2026-06-29", end_date: "2026-07-02" },
{ id: 2, task_id: 8, start_date: "2026-07-09", end_date: "2026-07-15" },
{ id: 3, task_id: 9, start_date: "2026-07-17", end_date: "2026-07-29" },
{ id: 4, task_id: 10, start_date: "2026-07-30", end_date: "2026-08-04" },
{ id: 5, task_id: 13, start_date: "2026-07-16", end_date: "2026-07-24" },
{ id: 6, task_id: 14, start_date: "2026-07-25", end_date: "2026-07-31" },
{ id: 7, task_id: 18, start_date: "2026-08-14", end_date: "2026-08-20" },
{ id: 8, task_id: 22, start_date: "2026-08-24", end_date: "2026-08-26" }
]
}
},
cloud: {
label: "Data Center",
title: "Data Center Capacity Expansion",
logo: "DC",
logoColor: "linear-gradient(140deg,#14b8a6,#0f766e)",
status: "Off track",
statusColor: "#ef4444",
marker_start: "2026-05-18",
marker_important: "2026-06-10",
data: {
tasks: [
{ id: 1, text: "Data Center Capacity Expansion", type: "project", progress: 0.31, open: true },
{ id: 2, text: "Site & Permits", type: "project", parent: 1, progress: 0.76, open: true },
{ id: 3, text: "Finalize capacity model", start_date: "2026-05-18", duration: 4, parent: 2, progress: 1, cost: 6000, resources: [{ resource_id: 1, value: 8 }, { resource_id: 2, value: 8 }], deadline: "2026-05-22" },
{ id: 4, text: "Utility interconnect review", start_date: "2026-05-22", duration: 6, parent: 2, progress: 0.9, cost: 10000, resources: [{ resource_id: 3, value: 8 }, { resource_id: 8, value: 8 }] },
{ id: 5, text: "Building permit package", start_date: "2026-05-27", duration: 8, parent: 2, progress: 0.58, cost: 15000, resources: [{ resource_id: 7, value: 8 }, { resource_id: 8, value: 8 }], deadline: "2026-06-05" },
{ id: 6, text: "Permit board hearing", start_date: "2026-06-10", type: "milestone", parent: 2, progress: 0 },
{ id: 7, text: "Long-Lead Procurement", type: "project", parent: 1, progress: 0.35, open: true },
{ id: 8, text: "Order switchgear", start_date: "2026-06-01", duration: 5, parent: 7, progress: 0.8, cost: 45000, resources: [{ resource_id: 2, value: 8 }, { resource_id: 30, value: 1 }], deadline: "2026-06-05" },
{ id: 9, text: "Cooling units procurement", start_date: "2026-06-08", duration: 7, parent: 7, progress: 0.36, cost: 68000, resources: [{ resource_id: 2, value: 8 }, { resource_id: 3, value: 8 }], deadline: "2026-06-16" },
{ id: 10, text: "Rack and cabling materials", start_date: "2026-06-15", duration: 5, parent: 7, progress: 0.12, cost: 22000, resources: [{ resource_id: 5, value: 8 }, { resource_id: 30, value: 1 }] },
{ id: 11, text: "Equipment delivery checkpoint", start_date: "2026-06-24", type: "milestone", parent: 7, progress: 0 },
{ id: 12, text: "Construction & Fit-Out", type: "project", parent: 1, progress: 0.22, open: true },
{ id: 13, text: "Prep server room and containment", start_date: "2026-06-11", duration: 8, parent: 12, progress: 0.45, cost: 32000, resources: [{ resource_id: 5, value: 8 }, { resource_id: 30, value: 1 }] },
{ id: 14, text: "Install electrical busways", start_date: "2026-06-23", duration: 7, parent: 12, progress: 0.18, cost: 42000, resources: [{ resource_id: 3, value: 8 }, { resource_id: 5, value: 8 }], deadline: "2026-07-01" },
{ id: 15, text: "Install cooling loop", start_date: "2026-07-02", duration: 8, parent: 12, progress: 0, cost: 52000, resources: [{ resource_id: 5, value: 8 }, { resource_id: 30, value: 1 }], deadline: "2026-07-13" },
{ id: 16, text: "Commission fire suppression", start_date: "2026-07-14", duration: 4, parent: 12, progress: 0, cost: 18000, resources: [{ resource_id: 3, value: 8 }, { resource_id: 8, value: 8 }] },
{ id: 17, text: "Network & Migration", type: "project", parent: 1, progress: 0.07, open: true },
{ id: 18, text: "Configure core network fabric", start_date: "2026-07-06", duration: 6, parent: 17, progress: 0, cost: 26000, resources: [{ resource_id: 4, value: 8 }, { resource_id: 6, value: 8 }] },
{ id: 19, text: "Migrate priority workloads", start_date: "2026-07-15", duration: 7, parent: 17, progress: 0, cost: 18000, resources: [{ resource_id: 1, value: 8 }, { resource_id: 4, value: 8 }] },
{ id: 20, text: "Disaster recovery drill", start_date: "2026-07-24", duration: 3, parent: 17, progress: 0, cost: 9000, resources: [{ resource_id: 6, value: 8 }, { resource_id: 7, value: 8 }], deadline: "2026-07-28" },
{ id: 21, text: "Capacity release gate", start_date: "2026-07-29", type: "milestone", parent: 17, progress: 0 },
{ id: 22, text: "Executive handover and cost review", start_date: "2026-07-30", duration: 2, parent: 17, progress: 0, cost: 3000, resources: [{ resource_id: 1, value: 8 }, { resource_id: 8, value: 8 }] }
],
links: [
{ id: 1, source: 3, target: 4, type: "0" },
{ id: 2, source: 4, target: 5, type: "0" },
{ id: 3, source: 5, target: 6, type: "0" },
{ id: 4, source: 6, target: 13, type: "0" },
{ id: 5, source: 8, target: 11, type: "0" },
{ id: 6, source: 9, target: 11, type: "0" },
{ id: 7, source: 10, target: 11, type: "0" },
{ id: 8, source: 11, target: 14, type: "0" },
{ id: 9, source: 13, target: 14, type: "0" },
{ id: 10, source: 14, target: 15, type: "0" },
{ id: 11, source: 15, target: 16, type: "0" },
{ id: 12, source: 14, target: 18, type: "0" },
{ id: 13, source: 16, target: 19, type: "0" },
{ id: 14, source: 18, target: 19, type: "0" },
{ id: 15, source: 19, target: 20, type: "0" },
{ id: 16, source: 20, target: 21, type: "0" },
{ id: 17, source: 21, target: 22, type: "0" }
],
baselines: [
{ id: 1, task_id: 5, start_date: "2026-05-25", end_date: "2026-06-03" },
{ id: 2, task_id: 8, start_date: "2026-05-28", end_date: "2026-06-03" },
{ id: 3, task_id: 9, start_date: "2026-06-04", end_date: "2026-06-12" },
{ id: 4, task_id: 13, start_date: "2026-06-08", end_date: "2026-06-17" },
{ id: 5, task_id: 14, start_date: "2026-06-19", end_date: "2026-06-29" },
{ id: 6, task_id: 15, start_date: "2026-06-30", end_date: "2026-07-09" },
{ id: 7, task_id: 18, start_date: "2026-07-01", end_date: "2026-07-08" },
{ id: 8, task_id: 19, start_date: "2026-07-10", end_date: "2026-07-20" },
{ id: 9, task_id: 20, start_date: "2026-07-21", end_date: "2026-07-24" }
]
}
}
};
let currentProject = "saas";
/* Expand the compact source schedules into multi-year plans without rewriting
every task date. One source day becomes 24 displayed days. */
const SCHEDULE_SCALE = 24;
const DAY_MS = 24 * 60 * 60 * 1000;
function scaleProjectDate(project, value) {
if (!value) return value;
const anchor = gantt.date.parseDate(project.marker_start, gantt.config.date_format);
const date = value instanceof Date ? value : gantt.date.parseDate(value, gantt.config.date_format);
const offsetDays = Math.round((date.valueOf() - anchor.valueOf()) / DAY_MS);
return gantt.date.add(anchor, offsetDays * SCHEDULE_SCALE, "day");
}
function prepareProjectData(project) {
const data = gantt.copy(project.data);
data.tasks.forEach((task) => {
if (task.start_date) task.start_date = scaleProjectDate(project, task.start_date);
if (task.duration) task.duration *= SCHEDULE_SCALE;
if (task.deadline) task.deadline = scaleProjectDate(project, task.deadline);
});
(data.baselines || []).forEach((baseline) => {
baseline.start_date = scaleProjectDate(project, baseline.start_date);
baseline.end_date = scaleProjectDate(project, baseline.end_date);
});
return data;
}
function renderProjectOptions() {
const menu = document.getElementById("project_menu");
const options = Object.entries(projects).map(([id, project]) => `
<div class="dhx_menu__item dhx_menu__item--project${id === currentProject ? " checked" : ""}" data-project="${id}">
<div class="dhx_menu__project-logo" style="background:${project.logoColor}">${project.logo}</div>
<div class="dhx_menu__project-info">
<div class="dhx_menu__project-title">${project.title}</div>
<div class="dhx_menu__project-meta"><i style="background:${project.statusColor}"></i>${project.status} . ${project.data.tasks.length} tasks</div>
</div>
<span class="dhx_menu__project-check">✓</span>
</div>`).join("");
menu.insertAdjacentHTML("beforeend", options);
menu.addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
const option = target?.closest("[data-project]");
if (option && menu.contains(option)) selectProject(option.dataset.project);
});
}
renderProjectOptions();
/* ---------------------------------------------------------------------------
* GRID COLUMN TEMPLATES
* ------------------------------------------------------------------------ */
const money = function (v) {
if (v == null || isNaN(v)) return "";
return `$${Number(v).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
};
function taskCost(task) {
if (gantt.isSummaryTask(task)) {
let sum = 0;
gantt.eachTask((child) => {
if (!gantt.isSummaryTask(child)) sum += (+child.cost || 0);
}, task.id);
return sum;
}
return +task.cost || 0;
}
function costTemplate(task) {
if (task.$group) return "";
const c = taskCost(task);
if (!c) return "";
const cls = gantt.isSummaryTask(task) ? "cost_cell summary" : "cost_cell";
return `<span class="${cls}">${money(c)}</span>`;
}
function resourcesTemplate(task) {
const list = task[gantt.config.resource_property];
if (!list || !list.length) return "";
let html = "<span class='avatars'>";
const maxShown = 2;
list.slice(0, maxShown).forEach((assignment) => {
const resource = resourceById[assignment.resource_id];
if (!resource) return;
html += avatarHtml(resource);
});
if (list.length > maxShown) {
const hiddenCount = list.length - maxShown;
html += `<span class="avatar more" title="${hiddenCount} more">+${hiddenCount}</span>`;
}
html += "</span>";
return html;
}
function progressTemplate(task) {
if (task.$group) return "";
if (task.type === gantt.config.types.milestone) return "";
const percent = Math.round((task.progress || 0) * 100);
const filled = Math.ceil(percent / 20);
const colors = ["", "#a5d6a7", "#66bb6a", "#43a047", "#2e7d32", "#1b5e20"];
let html = `<span class="progress_squares" title="${percent}%">`;
html += `<span class="pct-label">${percent}%</span>`;
for (let i = 0; i < 5; i++) {
const style = i < filled ? ` style="background:${colors[filled]}"` : "";
html += `<span class="sq"${style}></span>`;
}
html += "</span>";
return html;
}
function deadlineTemplate(task) {
if (task.$group || !task.deadline) return "";
const deadline = taskDeadline(task);
const text = gantt.templates.date_grid(deadline, task);
const overdue = task.end_date && task.end_date.valueOf() > deadline.valueOf();
return `${text}${overdue ? "<span class='overdue_dot' title='Overdue'>!</span>" : ""}`;
}
function freeSlackTemplate(task) {
if (task.$group || gantt.isSummaryTask(task)) return "";
return durationFormatter.format(gantt.getFreeSlack(task));
}
function totalSlackTemplate(task) {
if (task.$group) return "";
return durationFormatter.format(gantt.getTotalSlack(task));
}
function taskDeadline(task) {
if (!task.deadline) return null;
return task.deadline instanceof Date ? task.deadline : gantt.date.parseDate(task.deadline, gantt.config.date_format);
}
function isOverdue(end, task) {
const dl = taskDeadline(task);
return !!(end && dl && end.valueOf() > dl.valueOf());
}
const durationFormatter = gantt.ext.formatters.durationFormatter({
enter: "day",
store: "day",
format: "day",
short: false
});
const linksFormatter = gantt.ext.formatters.linkFormatter({ durationFormatter });
const inlineEditors = {
text: { type: "text", map_to: "text" },
start_date: { type: "date", map_to: "start_date" },
duration: { type: "duration", map_to: "duration", min: 0, max: 10000, formatter: durationFormatter },
deadline: { type: "date", map_to: "deadline" },
predecessors: { type: "predecessor", map_to: "auto", formatter: linksFormatter }
};
function predecessorsTemplate(task) {
const parts = [];
(task.$target || []).forEach((linkId) => {
const link = gantt.getLink(linkId);
if (!link || !gantt.isTaskExists(link.source)) return;
parts.push(linksFormatter.format(link));
});
return parts.join("; ");
}
gantt.config.columns = [
{ name: "text", label: "Name", tree: true, width: 280, resize: true, editor: inlineEditors.text, template: function (task) {
if (task.$group) return `<b>${task.text}</b>`;
return `<b>${gantt.getWBSCode(task)}</b> ${task.text}`;
}},
{ name: "cost", label: "Cost", align: "right", width: 100, resize: true, template: costTemplate },
{ name: "start_date", label: "Start", align: "center", width: 100, resize: true, editor: inlineEditors.start_date, template: function (task) {
return task.type === gantt.config.types.project || task.$group ? "" : gantt.templates.date_grid(task.start_date, task);
}},
{ name: "duration", label: "Duration", align: "center", width: 75, resize: true, editor: inlineEditors.duration, template: function (task) {
return task.type === gantt.config.types.milestone || task.$group ? "" : durationFormatter.format(task.duration);
}},
{ name: "resources", label: "Assigned Res...", align: "center", width: 110, resize: true, template: resourcesTemplate },
{ name: "progress", label: "% Done", align: "center", width: 100, resize: true, template: progressTemplate },
{ name: "deadline", label: "Deadline", align: "center", width: 100, resize: true, hide: true, editor: inlineEditors.deadline, template: deadlineTemplate },
{ name: "freeSlack", label: "Free slack", align: "center", width: 80, resize: true, hide: true, template: freeSlackTemplate },
{ name: "totalSlack", label: "Total slack", align: "center", width: 80, resize: true, hide: true, template: totalSlackTemplate },
{ name: "predecessors", label: "Predecessors", align: "left", width: 140, resize: true, hide: true, editor: inlineEditors.predecessors, template: predecessorsTemplate },
{ name: "add", width: 40, resize: true }
];
gantt.config.grid_width = 770;
gantt.templates.task_class = function (start, end, task) {
return isOverdue(end, task) ? "has_overdue" : "";
};
gantt.templates.rightside_text = function (start, end, task) {
if (!isOverdue(end, task)) return "";
const deadline = taskDeadline(task);
const days = gantt.calculateDuration({ start_date: deadline, end_date: end, task });
return `<b>Overdue: ${durationFormatter.format(days)}</b>`;
};
/* lightbox */
gantt.locale.labels.section_resources = "Resources";
gantt.locale.labels.section_deadline = "Deadline";
gantt.locale.labels.section_baselines = "Planned (baseline)";
gantt.config.lightbox.sections = [
{ name: "description", height: 38, map_to: "text", type: "textarea", focus: true },
{ name: "resources", map_to: "resources", type: "resources", options: resourceOptions },
{ name: "time", map_to: "auto", type: "duration" },
{ name: "deadline", map_to: "deadline", type: "duration_optional", button: true, single_date: true },
{ name: "baselines", height: 100, map_to: "baselines", type: "baselines" }
];
/* ---------------------------------------------------------------------------
* WEEKEND HIGHLIGHTING
* ------------------------------------------------------------------------ */
gantt.config.work_time = true;
gantt.templates.timeline_cell_class = function (task, date) {
return gantt.config.work_time && !gantt.isWorkTime({ date, task, unit: gantt.getScale().unit }) ? "gantt_weekend" : "";
};
gantt.templates.scale_cell_class = function (date) {
return gantt.config.work_time && !gantt.isWorkTime({ date }) ? "gantt_weekend" : "";
};
/* ---------------------------------------------------------------------------
* ZOOM (Day / Week / Month / Quarter / Year)
* ------------------------------------------------------------------------ */
gantt.config.scale_height = 54;
const dayToStr = gantt.date.date_to_str("%d %M %Y");
gantt.config.scales = [
{ unit: "week", step: 1, format: (date) => `Sun ${dayToStr(date)}` },
{ unit: "day", step: 1, format: "%D" }
];
const zoomNames = ["day", "week", "month", "quarter", "year"];
gantt.ext.zoom.init({
levels: [
{
name: "day",
scale_height: 54,
min_column_width: 48,
scales: [
{ unit: "week", step: 1, format: (date) => `${dayToStr(date)}` },
{ unit: "day", step: 1, format: "%D" }
]
},
{
name: "week",
scale_height: 54,
min_column_width: 62,
scales: [
{ unit: "month", step: 1, format: "%F %Y" },
{ unit: "week", step: 1, format: "W%W" }
]
},
{
name: "month",
scale_height: 54,
min_column_width: 68,
scales: [
{ unit: "year", step: 1, format: "%Y" },
{ unit: "month", step: 1, format: "%M" }
]
},
{
name: "quarter",
scale_height: 54,
min_column_width: 90,
scales: [
{ unit: "year", step: 1, format: "%Y" },
{ unit: "quarter", step: 1, format: (date) => `Q${Math.floor(date.getMonth() / 3) + 1}` }
]
},
{
name: "year",
scale_height: 54,
min_column_width: 82,
scales: [
{ unit: "year", step: 10, format: function (date) {
const firstYear = Math.floor(date.getFullYear() / 10) * 10;
return `${firstYear}–${firstYear + 9}`;
} },
{ unit: "year", step: 1, format: "%Y" }
]
}
],
fit: {
scope: "all",
padding: 1
},
element: () => gantt.$root.querySelector(".gantt_task")
});
DHX.zoom.init(gantt, zoomNames, "zoom_seg", "pm_scale");
function resetFitRange() {
gantt.ext.zoom.resetZoom();
}
function setZoom(name) {
resetFitRange();
DHX.zoom.set(name);
}
function zoomStep(dir) {
resetFitRange();
DHX.zoom.step(dir);
}
function zoomToFit() {
gantt.ext.zoom.zoomToFit({
scope: "all",
rangeMode: "target",
padding: 1
});
gantt.render();
DHX.zoom.syncUI();
}
/* ---------------------------------------------------------------------------
* TASK SEARCH + PRIORITY FILTER
* ------------------------------------------------------------------------ */
let searchValue = "";
function onSearch(value) {
searchValue = value || "";
gantt.render();
}
function matchesSearch(task) {
return DHX.search.match(task, searchValue, gantt);
}
const activePriorities = { high: true, medium: true, low: true };
function allPriorities() {
return activePriorities.high && activePriorities.medium && activePriorities.low;
}
function matchesPriority(task) {
if (allPriorities()) return true;
let ok = !!activePriorities[task.priority];
gantt.eachTask((child) => {
if (matchesPriority(child)) ok = true;
}, task.id);
return ok;
}
function togglePriority(p, el) {
activePriorities[p] = !activePriorities[p];
el.classList.toggle("checked", activePriorities[p]);
updatePriorityDots();
gantt.render();
}
function updatePriorityDots() {
const html = ["high", "medium", "low"]
.filter((priority) => activePriorities[priority])
.map((priority) => `<span class="dhx_dot dhx_dot--${priority}"></span>`)
.join("");
document.getElementById("pdots").innerHTML = html;
}
gantt.attachEvent("onBeforeTaskDisplay", function (id, task) {
if (task.$group) return true;
return matchesSearch(task) && matchesPriority(task);
});
/* ---------------------------------------------------------------------------
* GROUPING (Tree / By resource / By priority)
* ------------------------------------------------------------------------ */
let currentGroupMode = "tree";
function applyGroup(mode) {
if (mode === "tree") {
gantt.groupBy(false);
} else if (mode === "priority") {
gantt.groupBy({
groups: [
{ key: "high", label: "High priority" },
{ key: "medium", label: "Medium priority" },
{ key: "low", label: "Low priority" }
],
relation_property: "priority",
group_id: "key",
group_text: "label"
});
} else if (mode === "resource") {
const groups = resourceOptions.map((resource) => ({ key: String(resource.key), label: resource.label }));
groups.push({ key: "none", label: "Unassigned" });
gantt.groupBy({ groups: groups, relation_property: "_res", group_id: "key", group_text: "label" });
}
}
function setGroup(mode) {
currentGroupMode = mode;
DHX.ui.setText("group_label", mode === "resource" ? "By resource" : (mode === "priority" ? "By priority" : "Tree"));
document.querySelectorAll("#group_menu .dhx_menu__item").forEach(function (el) {
el.classList.toggle("checked", el.getAttribute("data-group") === mode);
});
applyGroup(mode);
DHX.dd.closeAll();
}
/* ---------------------------------------------------------------------------
* CRITICAL PATH
* ------------------------------------------------------------------------ */
let criticalOn = false;
function toggleCritical() {
criticalOn = !criticalOn;
DHX.toggle.setById("cp_switch", criticalOn);
gantt.config.highlight_critical_path = criticalOn;
gantt.render();
}
/* ---------------------------------------------------------------------------
* AUTO-SCHEDULING
* ------------------------------------------------------------------------ */
let autoScheduleOn = false;
function toggleAutoSchedule() {
autoScheduleOn = !autoScheduleOn;
DHX.toggle.setById("autoschedule_switch", autoScheduleOn);
gantt.config.auto_scheduling.enabled = autoScheduleOn;
if (autoScheduleOn) gantt.autoSchedule();
gantt.render();
}
function toggleWorkTime(el) {
gantt.config.work_time = !gantt.config.work_time;
el.classList.toggle("checked", gantt.config.work_time);
gantt.render();
}
/* ---------------------------------------------------------------------------
* SLACK / FLOAT (hatched bar drawn after each task)
* ------------------------------------------------------------------------ */
gantt.addTaskLayer(function drawSlack(task) {
if (!showSlack || task.$group) return null;
const slack = gantt.getFreeSlack(task);
if (!slack) return null;
const state = gantt.getState().drag_mode;
if (state === "resize" || state === "move") return null;
const slackStart = new Date(task.end_date);
const slackEnd = gantt.calculateEndDate({ start_date: slackStart, duration: slack, task });
const sizes = gantt.getTaskPosition(task, slackStart, slackEnd);
const el = document.createElement("div");
el.className = "slack";
el.style.left = `${sizes.left}px`;
el.style.top = `${sizes.top + 2}px`;
el.style.width = `${sizes.width}px`;
el.style.height = `${sizes.height}px`;
return el;
});
function toggleSlack(el) {
showSlack = !showSlack;
if (el) el.classList.toggle("checked", showSlack);
["freeSlack", "totalSlack"].forEach((name) => {
const col = gantt.getGridColumn(name);
if (col) col.hide = !showSlack;
});
gantt.render();
}
/* ---------------------------------------------------------------------------
* BASELINES (off / same row / separate row)
* ------------------------------------------------------------------------ */
function setBaselineMode(mode, el) {
gantt.config.baselines.render_mode = mode === "off" ? false : mode;
document.querySelectorAll("#baseline_menu .dhx_menu__item").forEach(function (item) {
item.classList.toggle("checked", item === el);
});
gantt.batchUpdate(function () {
gantt.eachTask(function (task) { gantt.adjustTaskHeightForBaselines(task); });
});
}
/* ---------------------------------------------------------------------------
* INLINE GRID EDITING
* ------------------------------------------------------------------------ */
let inlineEditOn = true;
function toggleInlineEdit(el) {
inlineEditOn = !inlineEditOn;
el.classList.toggle("checked", inlineEditOn);
["text", "start_date", "duration", "deadline", "predecessors"].forEach((name) => {
const col = gantt.getGridColumn(name);
if (col) col.editor = inlineEditOn ? inlineEditors[name] : undefined;
});
gantt.render();
}
/* ---------------------------------------------------------------------------
* THEME (light / dark)
* ------------------------------------------------------------------------ */
let darkTheme = false;
let lightSkin = "";
function toggleTheme() {
darkTheme = !darkTheme;
document.getElementById("theme_sun").classList.toggle("dhx_hidden", darkTheme);
document.getElementById("theme_moon").classList.toggle("dhx_hidden", !darkTheme);
gantt.setSkin(darkTheme ? "dark" : (lightSkin || "material"));
refreshMarkers();
gantt.render();
}
/* ---------------------------------------------------------------------------
* EXPORT (PDF / PNG / MS Project / Excel)
* ------------------------------------------------------------------------ */
function exportGantt(kind) {
DHX.dd.closeAll();
const name = projects[currentProject].label.toLowerCase().replace(/\s+/g, "-");
if (kind === "pdf") gantt.exportToPDF({ name: `${name}.pdf`, raw: true });
else if (kind === "png") gantt.exportToPNG({ name: `${name}.png`, raw: true });
else if (kind === "msproject") gantt.exportToMSProject({ name: name });
else if (kind === "excel") gantt.exportToExcel({ name: `${name}.xlsx` });
}
/* ---------------------------------------------------------------------------
* RESOURCE DATASTORE + column config
* ------------------------------------------------------------------------ */
const typeIcons = {
work: "<svg class='dhx_ic res_type_icon' viewBox='0 0 24 24'><circle cx='9' cy='8' r='3.2'/><path d='M3.5 19c0-3 2.5-5 5.5-5s5.5 2 5.5 5'/><path d='M16 6.6a3 3 0 0 1 0 5.6'/><path d='M17.4 14.2c2.1.5 3.6 2.3 3.6 4.8'/></svg>",
material: "<svg class='dhx_ic res_type_icon' viewBox='0 0 24 24'><path d='M21 8l-9-5-9 5 9 5 9-5z'/><path d='M3 8v8l9 5 9-5V8'/><line x1='12' y1='13' x2='12' y2='21'/></svg>",
cost: "<svg class='dhx_ic res_type_icon' viewBox='0 0 24 24'><line x1='12' y1='3' x2='12' y2='21'/><path d='M16 7.3C16 5.5 14.2 4.5 12 4.5S8 5.4 8 7.3s1.8 2.9 4 3.4 4 1.5 4 3.4-1.8 2.9-4 2.9-4-1-4-2.8'/></svg>"
};
function groupCost(groupId) {
let sum = 0;
const resourceStore = gantt.getDatastore(gantt.config.resource_store);
resourceStore.getChildren(groupId).forEach((id) => {
sum += (+resourceStore.getItem(id).cost || 0);
});
return sum;
}
function resourceTooltipTarget(resource, content, attributes) {
return `<span class="resource_tooltip_target" data-resource-id="${resource.id}"${attributes ? ` ${attributes}` : ""}>${content}</span>`;
}
function resourceLoadTooltipTarget(resource, start, end, tasks, allocated, capacity, content) {
const taskIds = (tasks || []).map((task) => task.id).join(",");
return resourceTooltipTarget(
resource,
content,
`data-start="${start.valueOf()}" data-end="${end.valueOf()}" data-task-ids="${taskIds}" data-allocated="${allocated}" data-capacity="${capacity}"`
);
}
const resourceConfig = {
columns: [
{ name: "name", label: "Name", tree: true, width: 220, resize: true, template: function (resource) {
if (resource.type === "group") {
const resourceStore = gantt.getDatastore(gantt.config.resource_store);
const count = resourceStore.getChildren(resource.id).length;
return resourceTooltipTarget(resource, `${typeIcons[resource.id.replace("g_", "")]} ${resource.text} (${count})`);
}
return resourceTooltipTarget(resource, `<span class="res_avatar_cell">${avatarHtml(resource)} ${resource.text}</span>`);
}},
{ name: "type", label: "Type", width: 90, resize: true, template: function (resource) { return resource.type === "group" ? "" : `${resource.type.charAt(0).toUpperCase()}${resource.type.slice(1)}`; } },
{ name: "material_label",label: "Material ...",width: 90, resize: true, template: function (resource) { return resource.material_label || ""; } },
{ name: "max_units", label: "Max units", align: "right", width: 80, resize: true, template: function (resource) { return resource.type === "group" ? "" : (resource.max_units != null ? resource.max_units : ""); } },
{ name: "accrue", label: "Accrue at", width: 90, resize: true, template: function (resource) { return resource.type === "group" ? "" : (resource.accrue || ""); } },
{ name: "cost", label: "Cost", align: "right", width: 110, resize: true, template: function (resource) { const cost = resource.type === "group" ? groupCost(resource.id) : resource.cost; return cost ? money(cost) : ""; } },
{ name: "calendar", label: "Calendar", width: 110, resize: true, template: function (resource) { return resource.calendar || ""; } },
{ name: "city", label: "City", width: 120, resize: true, template: function (resource) { return resource.city || ""; } },
{ name: "workload", label: "Total workload", align: "right", width: 130, resize: true, template: function (resource) { const workload = resourceWorkload(resource); return workload ? `${workload}h` : ""; } }
]
};
const resourceTemplates = {
grid_row_class: function (start, end, resource) {
return resource.type === "group" ? "folder_row group_row" : "";
}
};
/* ---------------------------------------------------------------------------
* RESOURCE LOAD TEMPLATES
* ------------------------------------------------------------------------ */
const WORK_DAY = 8;
function getTaskDurationInsideCell(cellStartDate, cellEndDate, task) {
const intersectionStartDate = new Date(
Math.max(cellStartDate.valueOf(), task.start_date.valueOf())
);
const intersectionEndDate = new Date(
Math.min(cellEndDate.valueOf(), task.end_date.valueOf())
);
if (intersectionStartDate >= intersectionEndDate) {
return 0;
}
return gantt.calculateDuration({
start_date: intersectionStartDate,
end_date: intersectionEndDate,
task: task
});
}
function getWorkingDaysInsideCell(cellStartDate, cellEndDate) {
return gantt.calculateDuration({
start_date: cellStartDate,
end_date: cellEndDate
});
}
function getAllocatedHoursInsideCell(cellStartDate, cellEndDate, tasks, resource) {
let totalAllocatedHours = 0;
(tasks || []).forEach((task) => {
const taskDurationInsideCell = getTaskDurationInsideCell(cellStartDate, cellEndDate, task);
if (!taskDurationInsideCell) {
return;
}
const resourceAssignments = gantt.getResourceAssignments(resource.id, task.id) || [];
resourceAssignments.forEach((resourceAssignment) => {
const assignedHoursPerDay = Number(resourceAssignment.value) || 0;
totalAllocatedHours += assignedHoursPerDay * taskDurationInsideCell;
});
});
return totalAllocatedHours;
}
function getResourceCapacityInsideCell(resource, cellStartDate, cellEndDate) {
if (resourceStore.hasChild(resource.id)) {
return -1;
}
if (resource.type !== "work") {
return -1;
}
const workingDaysInsideCell = getWorkingDaysInsideCell(cellStartDate, cellEndDate);
return WORK_DAY * workingDaysInsideCell;
}
function getAverageDailyHistogramValue(totalValue, cellStartDate, cellEndDate) {
const workingDaysInsideCell = getWorkingDaysInsideCell(cellStartDate, cellEndDate);
return workingDaysInsideCell ? totalValue / workingDaysInsideCell : 0;
}
gantt.templates.histogram_cell_class = function (cellStartDate, cellEndDate, resource, tasks) {
const allocatedHours = getAllocatedHoursInsideCell(cellStartDate, cellEndDate, tasks, resource);
const capacityHours = getResourceCapacityInsideCell(resource, cellStartDate, cellEndDate);
return capacityHours >= 0 && allocatedHours > capacityHours ? "column_overload" : "";
};
gantt.templates.histogram_cell_label = function (cellStartDate, cellEndDate, resource, tasks) {
if (resourceStore.hasChild(resource.id) || !tasks.length) {
return "";
}
const allocatedHours = getAllocatedHoursInsideCell(cellStartDate, cellEndDate, tasks, resource);
const capacityHours = getResourceCapacityInsideCell(resource, cellStartDate, cellEndDate);
const label = `${allocatedHours}` + `${capacityHours > 0 ? `/${capacityHours}` : ""}`;
return resourceLoadTooltipTarget(
resource,
cellStartDate,
cellEndDate,
tasks,
allocatedHours,
capacityHours,
label
);
};
gantt.templates.histogram_cell_allocated = function (cellStartDate, cellEndDate, resource, tasks) {
const allocatedHours = getAllocatedHoursInsideCell(cellStartDate, cellEndDate, tasks, resource);
return getAverageDailyHistogramValue(allocatedHours, cellStartDate, cellEndDate);
};
gantt.templates.histogram_cell_capacity = function (cellStartDate,cellEndDate, resource) {
const capacityHours = getResourceCapacityInsideCell(resource, cellStartDate, cellEndDate);
if (capacityHours < 0) {
return 0;
}
return getAverageDailyHistogramValue(capacityHours, cellStartDate, cellEndDate);
};
gantt.config.resource_render_empty_cells = true;
function hasResourceUsageData(tasks, resourceAssignments) {
return !!( (tasks && tasks.length) || (resourceAssignments && resourceAssignments.length));
}
gantt.templates.resource_cell_class = function (cellStartDate, cellEndDate, resource, tasks, resourceAssignments) {
if (resourceStore.hasChild(resource.id) || !hasResourceUsageData(tasks, resourceAssignments)) {
return "";
}
const allocatedHours = getAllocatedHoursInsideCell(cellStartDate, cellEndDate, tasks, resource);
const capacityHours = getResourceCapacityInsideCell(resource, cellStartDate, cellEndDate);
const isOverloaded =
capacityHours >= 0 && allocatedHours > capacityHours;
return `resource_usage_cell ${isOverloaded ? "usage_over" : "usage_ok"}`;
};
gantt.templates.resource_cell_value = function (cellStartDate, cellEndDate, resource, tasks, resourceAssignments) {
if (resourceStore.hasChild(resource.id) || !hasResourceUsageData(tasks, resourceAssignments)) {
return "";
}
let allocatedHours = getAllocatedHoursInsideCell(cellStartDate, cellEndDate, tasks, resource);
if (!allocatedHours) {
return "";
}
if (allocatedHours % 1) {
allocatedHours = Math.round(allocatedHours * 10) / 10;
}
const capacityHours = getResourceCapacityInsideCell(resource, cellStartDate, cellEndDate);
const tooltipTarget = resourceLoadTooltipTarget(
resource,
cellStartDate,
cellEndDate,
tasks,
allocatedHours,
capacityHours,
allocatedHours
);
return `<div>${tooltipTarget}</div>`;
};
function resourceNameColumn(width) {
return {
name: "name",
label: "Resource",
tree: true,
width: width || 240,
resize: true,
template: function (resource) {
if (resource.type === "group") {
return resourceTooltipTarget(
resource,
`${typeIcons[resource.id.replace("g_", "")]} <b>${resource.text}</b>`
);
}
return resourceTooltipTarget(
resource,
`<span class="res_avatar_cell">${avatarHtml(resource)} ${resource.text}</span>`
);
}
};
}
function resourceWorkload(resource) {
if (resource.type === "group") {
let sum = 0;
resourceStore.getChildren(resource.id).forEach((id) => {
sum += resourceWorkloadValue(id);
});
return sum;
}
return resourceWorkloadValue(resource.id);
}
function resourceWorkloadValue(id) {
let total = 0;
(gantt.ext.resources.getSummaryResourceAssignments(id) || []).forEach((assignment) => {
total += Number(assignment.value) * assignment.duration;
});
return total;
}
const resourcePanelConfig = {
columns: [
resourceNameColumn(240),
{
name: "capacity",
label: "Cap/day",
align: "center",
width: 80,
template: function (resource) {
return resource.type === "work" ? `${WORK_DAY}h` : "";
}
}
]
};
/* ---------------------------------------------------------------------------
* LAYOUTS
* ------------------------------------------------------------------------ */
function scrollableGridColumn(view, scrollX, scrollY, options) {
const config = options || {};
const column = {
rows: [
Object.assign({
view: view,
scrollX: scrollX,
scrollY: scrollY,
scrollable: true
}, config.view || {}),
{ view: "scrollbar", id: scrollX, scroll: "x", group: "horizontal" }
]
};
if (config.group) column.group = config.group;
if (config.width != null) column.width = config.width;
if (config.minWidth != null) column.minWidth = config.minWidth;
if (config.maxWidth != null) column.maxWidth = config.maxWidth;
return column;
}
function timelineColumn(view, scrollX, scrollY, options) {
const config = options || {};
return {
rows: [
{ cols: [
Object.assign({
view: view,
scrollX: scrollX,
scrollY: scrollY
}, config.view || {}),
{ view: "scrollbar", id: scrollY, scroll: "y", group: "vertical" }
]},
config.ownsHorizontal
? { view: "scrollbar", id: scrollX, scroll: "x", group: "horizontal" }
: { html: "", height: gantt.config.scroll_size }
]
};
}
function ganttRow(gravity) {
return {
gravity: gravity,
cols: [
scrollableGridColumn("grid", "gridScroll", "scrollVer", {
group: "grids",
width: gantt.config.grid_width,
minWidth: 260,
view: { id: "grid" }
}),
{ resizer: true, width: 1 },
timelineColumn("timeline", "scrollHor", "scrollVer", {
ownsHorizontal: true,
view: { id: "timeline" }
})
]
};
}
function resourceRow(view, viewConfig) {
return {
gravity: 1,
id: "res_panel",
config: resourcePanelConfig,
templates: resourceTemplates,
cols: [
scrollableGridColumn("resourceGrid", "resourceGridScroll", "resLoadVer", {
group: "grids",
width: gantt.config.grid_width,
minWidth: 260
}),
{ resizer: true, width: 1 },
timelineColumn(view, "scrollHor", "resLoadVer", {
view: viewConfig || {}
})
]
};
}
function ganttLayout() {
return {
css: "gantt_container",
rows: [ganttRow(1)]
};
}
function histogramLayout() {
return {
css: "gantt_container",
rows: [
ganttRow(2),
{ resizer: true, width: 1, next: "res_panel" },
resourceRow("resourceHistogram", { capacity: 16 })
]
};
}
function usageLayout() {
return {
css: "gantt_container",
rows: [
ganttRow(2),
{ resizer: true, width: 1, next: "res_panel" },
resourceRow("resourceTimeline")
]
};
}
function gridLayout() {
return {
css: "gantt_container",
rows: [
ganttRow(2),
{ resizer: true, width: 1, next: "res_panel" },
{
gravity: 1,
id: "res_panel",
config: resourceConfig,
templates: resourceTemplates,
rows: [
{ cols: [
{
view: "resourceGrid",
scrollX: "resourceGridScroll",
scrollY: "resLoadVer",
scrollable: true
},
{ view: "scrollbar", id: "resLoadVer", scroll: "y", group: "vertical" }
]},
{ view: "scrollbar", id: "resourceGridScroll", scroll: "x", group: "horizontal" }
]
}
]
};
}
const panelLayouts = { off: ganttLayout, histogram: histogramLayout, usage: usageLayout, grid: gridLayout };
const resourcePanelLabels = { off: "Off", histogram: "Histogram", usage: "Usage", grid: "Summary" };
gantt.config.layout = panelLayouts.histogram();
/* ---------------------------------------------------------------------------
* MARKERS
* ------------------------------------------------------------------------ */
let markersOn = true;
let markerIds = [];
function refreshMarkers() {
markerIds.forEach((id) => gantt.deleteMarker(id));
markerIds = [];
if (!markersOn) return;
const project = projects[currentProject];
const projectStart = scaleProjectDate(project, project.marker_start);
const importantDate = scaleProjectDate(project, project.marker_important);
markerIds.push(gantt.addMarker({
start_date: projectStart,
css: "project_start",
text: "Project start",
title: `Project start: ${dayToStr(projectStart)}`
}));
markerIds.push(gantt.addMarker({
start_date: importantDate,
css: "important_date",
text: "Go/No-go review",
title: `Go/No-go review: ${dayToStr(importantDate)}`
}));
}
/* ---------------------------------------------------------------------------
* HEADER / FOOTER META
* ------------------------------------------------------------------------ */
const stablePriorities = ["high", "medium", "low"];
gantt.attachEvent("onTaskLoading", (task) => {
if (!task.priority && !task.$virtual) task.priority = stablePriorities[(+task.id) % stablePriorities.length];
const assignments = task[gantt.config.resource_property];
task._res = assignments?.length ? String(assignments[0].resource_id) : "none";
if (task.deadline && !(task.deadline instanceof Date)) {
task.deadline = gantt.date.parseDate(task.deadline, gantt.config.date_format);
}
return true;
});
function buildHeaderAvatars() {
const workResources = resourcesData.filter((resource) => resource.type === "work");
const shown = workResources.slice(0, 3);
let html = shown.map((resource) => avatarHtml(resource)).join("");
const more = workResources.length - shown.length;
if (more > 0) html += `<span class="avatar more">+${more}</span>`;
document.getElementById("header_avatars").innerHTML = html;
}
function updateMeta() {
const count = gantt.getTaskCount();
const formatDay = gantt.date.date_to_str("%d %M %Y");
let min = null;
let max = null;
gantt.eachTask((task) => {
if (!task.start_date) return;
if (!min || task.start_date < min) min = task.start_date;
if (!max || task.end_date > max) max = task.end_date;
});
const range = min && max ? `${formatDay(min)} - ${formatDay(max)} . ` : "";
DHX.ui.setText("pm_subtitle", `${range}${count} tasks`);
DHX.ui.setText("footer_count", `${count} tasks`);
}
function fitLightboxToViewport() {
const lightbox = gantt.getLightbox();
if (!lightbox || lightbox.style.display === "none") return;
lightbox.style.top = `${Math.max(12, (window.innerHeight - lightbox.offsetHeight) / 2)}px`;
}
gantt.attachEvent("onLightbox", function () {
requestAnimationFrame(fitLightboxToViewport);
return true;
});
window.addEventListener("resize", fitLightboxToViewport);
/* ---------------------------------------------------------------------------
* INIT
* ------------------------------------------------------------------------ */
gantt.init("gantt_here");
lightSkin = gantt.skin || "";
/* resource type filter */
const activeTypes = { work: true, material: true, cost: true };
const resourceStore = gantt.getDatastore(gantt.config.resource_store);
resourceStore.attachEvent("onFilterItem", function (id, item) {
if (item.type === "group") return !!activeTypes[item.id.replace("g_", "")];
return !!activeTypes[item.type];
});
function toggleResType(type, btn) {
activeTypes[type] = !activeTypes[type];
btn.classList.toggle("checked", activeTypes[type]);
resourceStore.refresh();
}
gantt.ext.tooltips.tooltipFor({
selector: ".gantt_task_deadline",
html: function (e, node) {
const id = node.getAttribute("data-task-id");
if (!id || !gantt.isTaskExists(id)) return "";
const task = gantt.getTask(id);
const deadline = taskDeadline(task);
let html = `${task.text} . Deadline: ${dayToStr(deadline)}`;
if (task.end_date && deadline && task.end_date > deadline) {
const days = gantt.calculateDuration({ start_date: deadline, end_date: task.end_date, task });
html += ` . <span style="color:#f4a100">Overdue by ${durationFormatter.format(days)}</span>`;
}
return `<span>${html}</span>`;
}
});
gantt.ext.tooltips.tooltipFor({
selector: ".resource_tooltip_target",
html: function (e, node) {
const resource = resourceById[node.dataset.resourceId];
if (!resource) return "";
let html = `<div class="resource_tt"><div class="resource_tt__title">${resource.text}</div>`;
html += `<div class="resource_tt__row"><span>Type</span><b>${resource.type === "group" ? "Resource group" : resource.type}</b></div>`;
if (node.dataset.start) {
const start = new Date(Number(node.dataset.start));
const end = new Date(Number(node.dataset.end) - 1);
const allocated = Number(node.dataset.allocated);
const capacity = Number(node.dataset.capacity);
html += `<div class="resource_tt__row"><span>Period</span><b>${dayToStr(start)} – ${dayToStr(end)}</b></div>`;
html += `<div class="resource_tt__row"><span>Allocated</span><b>${allocated}h</b></div>`;
if (capacity >= 0) {
html += `<div class="resource_tt__row"><span>Capacity</span><b>${capacity}h</b></div>`;
}
const taskNames = (node.dataset.taskIds || "")
.split(",")
.filter((id) => id && gantt.isTaskExists(id))
.slice(0, 4)
.map((id) => gantt.getTask(id).text);
if (taskNames.length) {
html += `<div class="resource_tt__tasks"><b>Tasks:</b> ${taskNames.join(", ")}</div>`;
}
} else {
const workload = resourceWorkload(resource);
if (workload) html += `<div class="resource_tt__row"><span>Total workload</span><b>${workload}h</b></div>`;
if (resource.city) html += `<div class="resource_tt__row"><span>City</span><b>${resource.city}</b></div>`;
if (resource.cost) html += `<div class="resource_tt__row"><span>Cost</span><b>${money(resource.cost)}</b></div>`;
}
return html + "</div>";
}
});
selectProject("saas");
buildHeaderAvatars();
updatePriorityDots();
gantt.ext.zoom.setLevel("month");
DHX.zoom.syncUI();
document.getElementById("opt_inline").classList.add("checked");
/* ---------------------------------------------------------------------------
* CONTROL WIRING
* ------------------------------------------------------------------------ */
function setPanel(mode) {
if (!panelLayouts[mode]) mode = "histogram";
DHX.ui.setText("resource_panel_label", resourcePanelLabels[mode] || "Histogram");
document.querySelector("#dd_resource_panel .dhx_res_btn").classList.toggle("active", mode !== "off");
document.querySelectorAll("#resource_panel_menu [data-panel]").forEach(function (item) {
const active = item.getAttribute("data-panel") === mode;
item.classList.toggle("active", active);
item.classList.toggle("checked", active);
});
gantt.config.layout = panelLayouts[mode]();
gantt.init("gantt_here");
DHX.dd.closeAll();
}
function loadProject(id) {
currentProject = id;
gantt.clearAll();
const { tasks, links, baselines } = prepareProjectData(projects[id]);
gantt.parse({ tasks, links, baselines, resources: resourcesData });
refreshMarkers();
updateMeta();
if (currentGroupMode !== "tree") applyGroup(currentGroupMode);
}
function selectProject(id) {
loadProject(id);
const project = projects[id];
const logoEl = document.getElementById("pm_logo");
DHX.ui.setText("pm_title", project.title);
logoEl.textContent = project.logo;
logoEl.style.background = project.logoColor;
const statusEl = document.getElementById("pm_status");
document.getElementById("pm_status_dot").style.background = project.statusColor;
DHX.ui.setText("pm_status_text", project.status);
statusEl.style.color = project.statusColor;
document.querySelectorAll("#project_menu .dhx_menu__item--project").forEach(function (el) {
el.classList.toggle("checked", el.getAttribute("data-project") === id);
});
DHX.dd.closeAll();
gantt.showDate(scaleProjectDate(project, project.marker_start));
}
function toggleColumn(name, el) {
const col = gantt.getGridColumn(name);
if (!col) return;
col.hide = !col.hide;
el.classList.toggle("checked", !col.hide);
gantt.render();
}
function toggleMarkers(el) {
markersOn = !markersOn;
el.classList.toggle("checked", markersOn);
refreshMarkers();
}
/* dropdown click-outside closes all + hides the context menu */
DHX.dd.initClickOutside(hideContextMenu);
function fitSettingsMenu() {
const dropdown = document.getElementById("dd_settings");
const menu = document.getElementById("settings_menu");
if (!dropdown || !menu || getComputedStyle(menu).display === "none") {
return;
}
menu.classList.remove("dhx_menu--up");
menu.style.maxHeight = "";
const dropdownRect = dropdown.getBoundingClientRect();
const gap = 8;
const spaceBelow = window.innerHeight - dropdownRect.bottom - gap;
const spaceAbove = dropdownRect.top - gap;
const requiredHeight = Math.min(menu.scrollHeight, 320);
const openUp = spaceBelow < requiredHeight && spaceAbove > spaceBelow;
menu.classList.toggle("dhx_menu--up", openUp);
const availableHeight = openUp ? spaceAbove : spaceBelow;
menu.style.maxHeight = Math.max(120, availableHeight) + "px";
}
function toggleSettingsMenu() {
DHX.dd.toggle("dd_settings");
requestAnimationFrame(fitSettingsMenu);
}
window.addEventListener("resize", fitSettingsMenu);
/* ---------------------------------------------------------------------------
* CONTEXT MENU
* ------------------------------------------------------------------------ */
let ctxTaskId = null;
function hideContextMenu() {
const m = document.getElementById("task_ctx_menu");
if (m) m.style.display = "none";
}
gantt.attachEvent("onContextMenu", function (taskId, linkId, e) {
if (!taskId) return true;
ctxTaskId = taskId;
const menu = document.getElementById("task_ctx_menu");
const task = gantt.getTask(taskId);
menu.querySelector("[data-cmd='complete']").classList.toggle("checked", task.progress >= 1);
menu.style.display = "block";
const x = Math.min(e.clientX, window.innerWidth - menu.offsetWidth - 8);
const y = Math.min(e.clientY, window.innerHeight - menu.offsetHeight - 8);
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
return false;
});
function ctxCommand(cmd) {
hideContextMenu();
if (!ctxTaskId || !gantt.isTaskExists(ctxTaskId)) return;
const task = gantt.getTask(ctxTaskId);
if (cmd === "add") {
gantt.createTask({ text: "New task", start_date: task.start_date, duration: 3 }, ctxTaskId);
} else if (cmd === "duplicate") {
const copy = gantt.copy(task);
delete copy.id; delete copy.$target; delete copy.$source;
copy.text = `${task.text} (copy)`;
gantt.addTask(copy, gantt.getParent(task.id), gantt.getTaskIndex(ctxTaskId) + 1);
} else if (cmd === "complete") {
task.progress = task.progress >= 1 ? 0 : 1;
gantt.updateTask(ctxTaskId, task);
} else if (cmd === "delete") {
gantt.deleteTask(ctxTaskId);
}
ctxTaskId = null;
}<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Project dashboard: docked resource panel with switchable views</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>
html, body { height: 100%; margin: 0; overflow: hidden; }
body { display: flex; flex-direction: column; }
#gantt_here { width: 100%; min-height: 0; flex: 1; }
.gantt_weekend { background: rgba(0,0,0,.04); }
:root[data-gantt-theme="dark"] .gantt_weekend { background: rgba(255,255,255,.05); }
.gantt_task_deadline {
--dhx-gantt-base-colors-icons: #f4a100;
filter: drop-shadow(0 1px 3px rgba(0,0,0,0.30));
pointer-events: auto;
cursor: default;
}
.gantt_task_deadline:before{
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="17" height="20" viewBox="0 0 17 20"><polygon points="0,0 15,0 11,5.5 15,11 0,11" fill="%23f4a100"/><rect x="0" y="1" width="2" height="19" fill="%23f4a100"/></svg>');
}
.ctx_menu { position: fixed; z-index: 100; min-width: 170px; }
.overdue_dot {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 6px;
border-radius: 50%;
background: #e5484d;
color: #fff;
font: 700 11px var(--dhx-font);
vertical-align: middle;
}
.gantt_task_line.has_overdue { box-shadow: 0 0 0 2px rgba(229,72,77,.55); }
.gantt_task_line.has_overdue .gantt_task_content { color: #fff; }
.slack {
position: absolute;
border-radius: 0;
opacity: .7;
border: none;
border-right: 1px solid #b6b6b6;
margin-left: -2px;
background: repeating-linear-gradient(45deg, #fff, #fff 5px, #b6b6b6 5px, #b6b6b6 10px);
}
:root[data-gantt-theme="dark"] .slack {
border-right-color: #59616e;
background: repeating-linear-gradient(45deg, #2b313a, #2b313a 5px, #454d59 5px, #454d59 10px);
}
.cost_cell { font-variant-numeric: tabular-nums; }
.cost_cell.summary { font-weight: 600; }
.avatars { display: inline-flex; align-items: center; }
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
color: #fff;
font: 600 11px var(--dhx-font);
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: -8px;
border: 2px solid var(--dhx-panel);
box-shadow: 0 1px 2px rgba(16,24,40,.18);
box-sizing: border-box;
}
.avatar:first-child { margin-left: 0; }
.avatar.more { background: #8a94a0; font-size: 10px; }
.progress_squares { display: inline-flex; align-items: center; gap: 2px; }
.progress_squares .sq { width: 7px; height: 14px; border-radius: 2px; background: #c2cad4; }
.progress_squares .pct-label {
font: 500 12px var(--dhx-font);
color: #37474f;
width: 28px;
text-align: right;
margin-right: 4px;
flex: none;
}
.folder_row { font-weight: 600; }
.group_row, .group_row.odd { background-color: rgba(232,232,232,.5); }
:root[data-gantt-theme="dark"] .group_row,
:root[data-gantt-theme="dark"] .group_row.odd { background-color: rgba(255,255,255,.07); }
.res_type_icon { margin-right: 6px; opacity: .65; }
.res_avatar_cell { display: inline-flex; align-items: center; gap: 8px; }
.resource_usage_cell { text-align: center; }
.resource_usage_cell > div {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
margin: 4px auto 0;
padding: 0 5px;
border-radius: 6px;
font: 600 11px var(--dhx-font);
box-sizing: border-box;
}
.resource_usage_cell.usage_ok > div { background: var(--dhx-ok-bg); color: var(--dhx-ok-ink); }
.resource_usage_cell.usage_over > div { background: var(--dhx-danger-bg); color: var(--dhx-danger-ink); }
.gantt_marker.important_date { background: #f4a100; }
.gantt_marker.project_start { background: #1565c0; }
.legend { display: flex; align-items: center; gap: 16px; }
.legend .lg { display: inline-flex; align-items: center; gap: 6px; }
.legend .sw { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }
.legend .sw.project { background: #27ae60; }
.legend .sw.task { background: #3b7be6; }
.legend .sw.milestone { width: 9px; height: 9px; border-radius: 2px; background: #d33daf; transform: rotate(45deg); }
.footer_right { display: flex; align-items: center; gap: 16px; margin-left: auto; }
.gantt_cal_light {
max-height: calc(100vh - 24px) !important;
overflow: hidden;
}
.gantt_cal_larea {
max-height: calc(100vh - 154px) !important;
overflow-y: auto !important;
overscroll-behavior: contain;
}
.resource_tooltip_target { cursor: help; }
.resource_tt { min-width: 210px; line-height: 1.45; }
.resource_tt__title { font-weight: 700; margin-bottom: 5px; }
.resource_tt__row { display: flex; justify-content: space-between; gap: 18px; }
.resource_tt__row span { opacity: .72; }
.resource_tt__tasks { margin-top: 5px; max-width: 320px; }
.dhx_header,
.dhx_toolbar {
flex: 0 0 auto;
box-sizing: border-box;
max-width: 100%;
height: auto;
flex-wrap: wrap;
row-gap: 8px;
overflow: visible;
}
.dhx_header { min-height: 60px; }
.dhx_toolbar { min-height: 48px; }
.dhx_header__right { min-width: 0; flex-wrap: wrap; }
.dhx_header__center { flex-wrap: wrap; }
.dhx_brand__title {
display: block;
max-width: min(34vw, 360px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#settings_menu {
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
#dd_settings #settings_menu.dhx_menu--up {
top: auto;
bottom: calc(100% + 6px);
}
@media (max-width: 1450px) {
.dhx_header__center {
position: static;
left: auto;
transform: none;
order: 3;
flex: 1 1 100%;
justify-content: center;
margin: 0;
}
}
@media (max-width: 1050px) {
.dhx_header__right {
flex: 1 1 100%;
justify-content: center;
}
.dhx_toolbar { justify-content: center; }
.dhx_toolbar .dhx_spacer { display: none; }
}
@media (max-width: 720px) {
#zoom_seg {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
width: 100%;
}
#zoom_seg .dhx_btn { width: 100%; }
.dhx_header__center { width: 100%; }
.dhx_header__right .dhx_search { flex: 1 1 100%; }
.dhx_header__right .dhx_chips { display: none; }
.dhx_brand__title { max-width: 230px; }
.legend { display: none; }
}
</style>
</head>
<body>
<!-- Top header bar -->
<div class="dhx_header">
<!-- Project selector dropdown -->
<div class="dhx_dd" id="dd_project">
<div class="dhx_brand">
<button class="dhx_brand__btn" onclick="DHX.dd.toggle('dd_project')">
<div class="dhx_brand__logo" id="pm_logo">AI</div>
<div class="dhx_brand__titles">
<div class="dhx_brand__title-row">
<span class="dhx_brand__title" id="pm_title">Enterprise AI Rollout</span>
<span class="dhx_caret"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></span>
</div>
<div class="dhx_brand__meta">
<span class="dhx_brand__status" id="pm_status"><i id="pm_status_dot"></i><span id="pm_status_text">On track</span></span>
<span class="dhx_brand__subtitle" id="pm_subtitle"></span>
</div>
</div>
</button>
</div>
<div class="dhx_menu" id="project_menu">
<div class="dhx_menu__label">Switch project</div>
</div>
</div>
<!-- Zoom segmented control + Today button -->
<div class="dhx_header__center">
<div class="dhx_seg" id="zoom_seg">
<button class="dhx_btn" data-zoom="day" onclick="setZoom('day')">Day</button>
<button class="dhx_btn" data-zoom="week" onclick="setZoom('week')">Week</button>
<button class="dhx_btn" data-zoom="month" onclick="setZoom('month')">Month</button>
<button class="dhx_btn" data-zoom="quarter" onclick="setZoom('quarter')">Quarter</button>
<button class="dhx_btn" data-zoom="year" onclick="setZoom('year')">Year</button>
</div>
<button class="dhx_btn dhx_btn--surface" title="Scroll to today" onclick="gantt.showDate(new Date())">Today</button>
<button class="dhx_btn dhx_btn--surface" title="Fit all tasks into the timeline" onclick="zoomToFit()">Zoom to fit</button>
</div>
<!-- Search + team avatars + New task -->
<div class="dhx_header__right">
<div class="dhx_search">
<svg class="dhx_ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/></svg>
<input id="search_input" type="text" placeholder="Search tasks" oninput="onSearch(this.value)">
</div>
<div class="dhx_chips" id="header_avatars"></div>
<button class="dhx_btn dhx_btn--primary" onclick="gantt.createTask()">
<svg class="dhx_ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
New task
</button>
</div>
</div>
<!-- Main toolbar -->
<div class="dhx_toolbar">
<!-- Resource panel switcher -->
<div class="dhx_dd" id="dd_resource_panel">
<button class="dhx_res_btn" onclick="DHX.dd.toggle('dd_resource_panel')" title="Resource panel controls">
<svg class="dhx_ic" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="1.5"/><line x1="8" y1="5" x2="8" y2="19"/><line x1="12" y1="5" x2="12" y2="19"/><line x1="16" y1="5" x2="16" y2="19"/></svg>
Resources: <b id="resource_panel_label">Histogram</b>
<span class="dhx_caret"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="dhx_menu dhx_res_menu" id="resource_panel_menu">
<div class="dhx_menu__label">Resource panel</div>
<div class="dhx_menu__item" data-panel="off" onclick="setPanel('off')">
<span class="dhx_res_menu__icon"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><rect x="3.5" y="4" width="17" height="16" rx="2"/><line x1="3.5" y1="14" x2="20.5" y2="14"/></svg></span>
Off
</div>
<div class="dhx_menu__item active checked" data-panel="histogram" onclick="setPanel('histogram')">
<span class="dhx_res_menu__icon"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><line x1="4" y1="20" x2="20" y2="20"/><rect x="5.5" y="11" width="3" height="7"/><rect x="10.5" y="6" width="3" height="12"/><rect x="15.5" y="13" width="3" height="5"/></svg></span>
Histogram
</div>
<div class="dhx_menu__item" data-panel="usage" onclick="setPanel('usage')">
<span class="dhx_res_menu__icon"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><rect x="3.5" y="4.5" width="17" height="15" rx="1.5"/><line x1="3.5" y1="9.5" x2="20.5" y2="9.5"/><line x1="9" y1="9.5" x2="9" y2="19.5"/><line x1="14.5" y1="9.5" x2="14.5" y2="19.5"/></svg></span>
Usage
</div>
<div class="dhx_menu__item" data-panel="grid" onclick="setPanel('grid')">
<span class="dhx_res_menu__icon"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="17" x2="20" y2="17"/></svg></span>
Summary
</div>
<div class="dhx_menu__divider"></div>
<div class="dhx_menu__label">Display resource types</div>
<div class="dhx_menu__item dhx_menu__item--check checked" data-type="work" onclick="toggleResType('work', this)">
<span class="dhx_res_menu__icon">♛</span>
Work
</div>
<div class="dhx_menu__item dhx_menu__item--check checked" data-type="material" onclick="toggleResType('material', this)">
<span class="dhx_res_menu__icon">📦</span>
Material
</div>
<div class="dhx_menu__item dhx_menu__item--check checked" data-type="cost" onclick="toggleResType('cost', this)">
<span class="dhx_res_menu__icon">$</span>
Cost
</div>
</div>
</div>
<!-- Toggle switches -->
<label class="dhx_toggle" onclick="toggleCritical()">
<span>Critical path</span>
<span class="dhx_toggle__track" id="cp_switch"></span>
</label>
<label class="dhx_toggle" onclick="toggleAutoSchedule()">
<span>Auto-scheduling</span>
<span class="dhx_toggle__track" id="autoschedule_switch"></span>
</label>
<!-- Priority filter -->
<div class="dhx_dd" id="dd_priority">
<button class="dhx_btn" onclick="DHX.dd.toggle('dd_priority')">
<svg class="dhx_ic" viewBox="0 0 24 24"><line x1="5" y1="3" x2="5" y2="21"/><path d="M5 4.2h11l-2.6 3.4L16 11H5"/></svg>
Priority <span class="dhx_dots" id="pdots"></span>
<span class="dhx_caret"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="dhx_menu" id="priority_menu">
<div class="dhx_menu__label">Show priority</div>
<div class="dhx_menu__item dhx_menu__item--check checked" data-prio="high" onclick="togglePriority('high', this)"><span class="dhx_dot dhx_dot--high"></span>High</div>
<div class="dhx_menu__item dhx_menu__item--check checked" data-prio="medium" onclick="togglePriority('medium', this)"><span class="dhx_dot dhx_dot--medium"></span>Medium</div>
<div class="dhx_menu__item dhx_menu__item--check checked" data-prio="low" onclick="togglePriority('low', this)"><span class="dhx_dot dhx_dot--low"></span>Low</div>
</div>
</div>
<!-- Grouping -->
<div class="dhx_dd" id="dd_group">
<button class="dhx_btn" onclick="DHX.dd.toggle('dd_group')">
<svg class="dhx_ic" viewBox="0 0 24 24"><path d="M12 3l8 4-8 4-8-4 8-4z"/><path d="M4 12l8 4 8-4"/><path d="M4 17l8 4 8-4"/></svg>
Group: <b id="group_label">Tree</b>
<span class="dhx_caret"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="dhx_menu" id="group_menu">
<div class="dhx_menu__item checked" data-group="tree" onclick="setGroup('tree')">Tree</div>
<div class="dhx_menu__item" data-group="resource" onclick="setGroup('resource')">By resource</div>
<div class="dhx_menu__item" data-group="priority" onclick="setGroup('priority')">By priority</div>
</div>
</div>
<!-- Zoom step buttons -->
<button class="dhx_btn dhx_btn--icon" title="Zoom in" onclick="zoomStep(1)">
<svg class="dhx_ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/><line x1="11" y1="8.5" x2="11" y2="13.5"/><line x1="8.5" y1="11" x2="13.5" y2="11"/></svg>
</button>
<button class="dhx_btn dhx_btn--icon" title="Zoom out" onclick="zoomStep(-1)">
<svg class="dhx_ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/><line x1="8.5" y1="11" x2="13.5" y2="11"/></svg>
</button>
<div class="dhx_spacer"></div>
<!-- Undo / Redo -->
<button class="dhx_btn dhx_btn--icon" title="Undo" onclick="gantt.undo()">
<svg class="dhx_ic" viewBox="0 0 24 24"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5 5.5 5.5 0 0 1-5.5 5.5H11"/></svg>
</button>
<button class="dhx_btn dhx_btn--icon" title="Redo" onclick="gantt.redo()">
<svg class="dhx_ic" viewBox="0 0 24 24"><path d="M15 14l5-5-5-5"/><path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5 5.5 5.5 0 0 0 9.5 20H13"/></svg>
</button>
<!-- Export -->
<div class="dhx_dd" id="dd_export">
<button class="dhx_btn" onclick="DHX.dd.toggle('dd_export')">
<svg class="dhx_ic" viewBox="0 0 24 24"><path d="M12 3v12"/><polyline points="8 11 12 15 16 11"/><path d="M5 19h14"/></svg>
Export
<span class="dhx_caret"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="dhx_menu dhx_menu--right" id="export_menu">
<div class="dhx_menu__item" onclick="exportGantt('pdf')">PDF</div>
<div class="dhx_menu__item" onclick="exportGantt('png')">PNG image</div>
<div class="dhx_menu__item" onclick="exportGantt('msproject')">MS Project</div>
<div class="dhx_menu__item" onclick="exportGantt('excel')">Excel</div>
</div>
</div>
<!-- Theme toggle -->
<button class="dhx_btn dhx_btn--icon" title="Toggle theme" onclick="toggleTheme()">
<svg class="dhx_ic" id="theme_sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4.2"/><line x1="12" y1="2.5" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="21.5"/><line x1="2.5" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="21.5" y2="12"/><line x1="5.3" y1="5.3" x2="7" y2="7"/><line x1="17" y1="17" x2="18.7" y2="18.7"/><line x1="5.3" y1="18.7" x2="7" y2="17"/><line x1="17" y1="7" x2="18.7" y2="5.3"/></svg>
<svg class="dhx_ic dhx_hidden" id="theme_moon" viewBox="0 0 24 24"><path d="M20 14.5A8 8 0 0 1 9.5 4a7 7 0 1 0 10.5 10.5z"/></svg>
</button>
<div class="dhx_sep"></div>
<!-- Settings / overflow -->
<div class="dhx_dd" id="dd_settings">
<button class="dhx_btn dhx_btn--icon" title="More" onclick="toggleSettingsMenu()">
<svg class="dhx_ic" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.5" fill="currentColor" stroke="none"/></svg>
</button>
<div class="dhx_menu dhx_menu--right" id="settings_menu">
<div class="dhx_menu__label">Columns</div>
<div class="dhx_menu__item dhx_menu__item--col checked" data-col="cost" onclick="toggleColumn('cost', this)">Cost</div>
<div class="dhx_menu__item dhx_menu__item--col checked" data-col="resources" onclick="toggleColumn('resources', this)">Assigned resources</div>
<div class="dhx_menu__item dhx_menu__item--col checked" data-col="progress" onclick="toggleColumn('progress', this)">% Done</div>
<div class="dhx_menu__item dhx_menu__item--col" data-col="deadline" onclick="toggleColumn('deadline', this)">Deadline</div>
<div class="dhx_menu__item dhx_menu__item--col" data-col="predecessors" onclick="toggleColumn('predecessors', this)">Predecessors</div>
<div class="dhx_menu__label">View options</div>
<div class="dhx_menu__item dhx_menu__item--check" id="opt_slack" onclick="toggleSlack(this)">Slack</div>
<div class="dhx_menu__item dhx_menu__item--check" id="opt_inline" onclick="toggleInlineEdit(this)">Inline grid editing</div>
<div class="dhx_menu__item dhx_menu__item--check checked" id="opt_worktime" onclick="toggleWorkTime(this)">Work time</div>
<div class="dhx_menu__label">Baselines (plan vs actual)</div>
<div id="baseline_menu">
<div class="dhx_menu__item dhx_menu__item--check checked" onclick="setBaselineMode('off', this)">Off</div>
<div class="dhx_menu__item dhx_menu__item--check" onclick="setBaselineMode('taskRow', this)">Same row</div>
<div class="dhx_menu__item dhx_menu__item--check" onclick="setBaselineMode('separateRow', this)">Separate row</div>
<div class="dhx_menu__item dhx_menu__item--check" onclick="setBaselineMode('individualRow', this)">Individual row</div>
</div>
<div class="dhx_menu__label">Timeline</div>
<div class="dhx_menu__item checked" id="opt_markers" onclick="toggleMarkers(this)">Show markers</div>
</div>
</div>
</div>
<div id="gantt_here"></div>
<!-- Right-click context menu -->
<div class="dhx_menu ctx_menu" id="task_ctx_menu">
<div class="dhx_menu__item" onclick="ctxCommand('add')">Add child task</div>
<div class="dhx_menu__item" onclick="ctxCommand('duplicate')">Duplicate</div>
<div class="dhx_menu__item" data-cmd="complete" onclick="ctxCommand('complete')">Mark complete</div>
<div class="dhx_menu__item dhx_menu__item--danger" onclick="ctxCommand('delete')">Delete</div>
</div>
<!-- Bottom status bar -->
<div class="dhx_footer">
<span id="footer_count">22 tasks</span>
<div class="legend">
<span class="lg"><i class="sw project"></i> Project</span>
<span class="lg"><i class="sw task"></i> Task</span>
<span class="lg"><i class="sw milestone"></i> Milestone</span>
</div>
<div class="footer_right">
<span id="pm_scale">Day scale</span>
<span>100%</span>
</div>
</div>
</body>
</html>