Skip to main content
Back to examples
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">&#x2713;</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">&#9819;</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">&#128230;</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>