React/Resource panel
Loading live demo…from dhtmlx.github.io
React
Resource panel
Show resource assignments and a histogram panel alongside the Gantt timeline.
- Demo.tsx
- demoData.ts
- ResourceSelectDropdown.tsx
- useResourceHistogram.ts
- styles.css
src/examples/resource-panel/Demo.tsx View on GitHub
import React, { useRef, useEffect, useState } from 'react';
import ReactGantt, { ReactGanttRef, GanttTemplates, Task, ResourceItem, useWorkTime, GanttConfig } from '@dhtmlx/trial-react-gantt';
import { useResourceHistogram } from './hooks/useResourceHistogram';
import { useResourceAssignments, useGanttDatastore } from '@dhtmlx/trial-react-gantt';
import ResourceSelectDropdown from './ResourceSelectDropdown';
import demoData from './demoData';
import './styles.css';
export default function ResourceHistogramGantt(props: any) {
const ganttRef = useRef<ReactGanttRef>(null);
useEffect(() => {
document.title = "DHTMLX React Gantt | Resource Panel";
}, []);
const { tasks, links, resources } = demoData;
const [resourceFilter, setResourceFilter ] = useState<((res: any) => boolean)|null>(null);
const [selectedResource, setSelectedResource ] = useState<string | null>(null);
const { isWorkTime, calculateDuration } = useWorkTime(ganttRef);
const { getResourceAssignments } = useResourceAssignments(ganttRef);
const { getItem: getTask } = useGanttDatastore<Task>(ganttRef, "task");
const { getItem: getResource, hasChild: resourceHasChild, getChildren: getResourceChildren } = useGanttDatastore<ResourceItem>(ganttRef, "resource");
const { getAllocatedValue, getCapacity, isGroupResource } = useResourceHistogram(ganttRef);
const templates: GanttTemplates = {
timeline_cell_class: (task, date) => {
return isWorkTime({ date, task }) ? '' : 'week_end';
},
histogram_cell_class: (start, _end, resource, tasks) => {
const allocated = getAllocatedValue(tasks, resource);
const cap = getCapacity(start, resource);
return allocated > cap ? 'column_overload' : '';
},
histogram_cell_label: (start, _end, resource, tasks) => {
if (isGroupResource(resource)) return '';
const allocated = getAllocatedValue(tasks, resource);
const cap = getCapacity(start, resource);
return tasks.length ? `${allocated}/${cap}` : '–';
},
histogram_cell_allocated: (start, _end, resource, tasks) => {
return getAllocatedValue(tasks, resource);
},
histogram_cell_capacity: (start, _end, resource) => {
if (!isWorkTime({ date: start })) return 0;
return getCapacity(start, resource);
},
grid_row_class: (_, __, resource) => {
if (isGroupResource(resource)) return 'folder_row group_row';
return '';
},
task_row_class: (_, __, resource) => {
if (isGroupResource(resource)) return 'group_row';
return '';
},
};
const resourceTemplates: GanttTemplates = {
};
const resourceConfig: GanttConfig = {
scale_height: 30,
row_height: 45,
scales: [
{ unit: "day", step: 1, date: "%d %M" }
],
columns: [
{
name: "name", label: "Name", tree: true, width: 200, template: function (resource) {
return resource.text;
}, resize: true
},
{
name: "progress", label: "Complete", align: "center", template: function (resource) {
let totalToDo = 0,
totalDone = 0;
const assignments = getResourceAssignments(resource.id);
assignments.forEach(function (assignment) {
const task = getTask(assignment.task_id);
totalToDo += (task.duration || 1);
totalDone += (task.duration || 1) * (task.progress || 0);
});
let completion = 0;
if (totalToDo) {
completion = (totalDone / totalToDo) * 100;
}
return Math.floor(completion) + "%";
}, resize: true
},
{
name: "workload", label: "Workload", align: "center", template: function (resource) {
let totalDuration = 0;
const assignments = getResourceAssignments(resource.id);
assignments.forEach(function (assignment) {
const task = getTask(assignment.task_id);
totalDuration += Number(assignment.value) * (task.duration || 1);
});
return (totalDuration || 0) + "h";
}, resize: true
},
{
name: "capacity", label: "Capacity", align: "center", template: function (resource) {
const gantt = ganttRef.current?.instance;
if (!gantt) return;
const n = resourceHasChild(resource.id) ? getResourceChildren(resource.id).length : 1
const state = gantt.getState();
return gantt.calculateDuration(state.min_date, state.max_date) * n * 8 + "h";
}
}
]
};
function handleResourceSelectChange(resourceId: string | null) {
setSelectedResource(resourceId);
if(resourceId === null){
setResourceFilter(null);
}else{
setResourceFilter(() => (resource: ResourceItem) => String(resource.id) === String(resourceId));
}
}
const config = {
// columns in the main grid
columns: [
{ name: 'text', tree: true, width: 200, resize: true },
{ name: 'start_date', align: 'center', width: 100, resize: true },
{
name: 'owner', align: 'center', width: 75, label: 'Owner',
template: (task: Task) => {
const gantt = ganttRef.current?.instance;
if (!gantt) return '';
if (task.type === gantt.config.types.project) return '';
const resources = gantt.getTaskResources(task.id) || [];
if (!resources.length) {
return 'Unassigned';
} else if (resources.length === 1) {
return resources[0].text;
} else {
return resources.map((r: any) =>
`<div class='owner-label' title='${r.text}'>${r.text.charAt(0)}</div>`
).join('');
}
},
resize: true
},
{ name: 'duration', width: 60, align: 'center', resize: true },
{ name: 'add', width: 44 }
],
// resource-related config for Resource Diagram + Histogram
resources: true,
resource_store: 'resource',
resource_property: 'owner',
resource_render_empty_cells: true,
order_branch: true,
open_tree_initially: true,
// sample lightbox sections
lightbox: {
sections: [
{ name: 'description', height: 38, map_to: 'text', type: 'textarea', focus: true },
{ name: 'resources', label: "Resources", type: 'resources', map_to: 'auto', default_value: 8 },
{ name: 'time', type: 'duration', map_to: 'auto' }
]
},
layout: {
css: "gantt_container",
rows: [
{
gravity: 2,
cols: [
{ view: "grid", group: "grids", scrollY: "scrollVer" },
{ resizer: true, width: 1 },
{ view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer" },
{ view: "scrollbar", id: "scrollVer", group: "vertical" }
]
},
{ resizer: true, width: 1, next: "resources" },
{
height: 60,
cols: [
{ html: <ResourceSelectDropdown
resources={resources}
initialValue={selectedResource}
onChange={handleResourceSelectChange}
/>, css: "resource-select-panel", group: "grids", id: "resourceSelectDropdown" },
{ resizer: true, width: 1 },
{ html: "" }
]
},
{
gravity: 1,
id: "resources",
config: resourceConfig,
templates: resourceTemplates,
cols: [
{ view: "resourceGrid", group: "grids", scrollY: "resourceVScroll" },
{ resizer: true, width: 1 },
{ view: "resourceHistogram", capacity: 24, scrollX: "scrollHor", scrollY: "resourceVScroll" },
{ view: "scrollbar", id: "resourceVScroll", group: "vertical" }
]
},
{ view: "scrollbar", id: "scrollHor" }
]
}
};
return (
<ReactGantt
ref={ganttRef}
tasks={tasks}
links={links}
resources={resources}
resourceFilter={resourceFilter}
config={config}
templates={templates}
plugins={{ auto_scheduling: true }}
/>
);
}
src/examples/resource-panel/demoData.ts View on GitHub
export default {
tasks: [
{ id: 1, text: "Office itinerancy", type: "project", start_date: "02-04-2025 00:00", duration: 17, progress: 0.4, owner: [{ resource_id: "5", value: 3 }], parent: 0 },
{ id: 2, text: "Office facing", type: "project", start_date: "02-04-2025 00:00", duration: 8, progress: 0.6, owner: [{ resource_id: "5", value: 4 }], parent: "1" },
{ id: 3, text: "Furniture installation", type: "project", start_date: "11-04-2025 00:00", duration: 8, parent: "1", progress: 0.6, owner: [{ resource_id: "5", value: 2 }] },
{ id: 4, text: "The employee relocation", type: "project", start_date: "13-04-2025 00:00", duration: 5, parent: "1", progress: 0.5, owner: [{ resource_id: "5", value: 4 }], priority: 3 },
{ id: 5, text: "Interior office", type: "task", start_date: "03-04-2025 00:00", duration: 7, parent: "2", progress: 0.6, owner: [{ resource_id: "6", value: 5 }], priority: 1 },
{ id: 6, text: "Air conditioners check", type: "task", start_date: "03-04-2025 00:00", duration: 7, parent: "2", progress: 0.6, owner: [{ resource_id: "7", value: 1 }], priority: 2 },
{ id: 7, text: "Workplaces preparation", type: "task", start_date: "12-04-2025 00:00", duration: 8, parent: "3", progress: 0.6, owner: [{ resource_id: "10", value: 2 }] },
{ id: 8, text: "Preparing workplaces", type: "task", start_date: "14-04-2025 00:00", duration: 5, parent: "4", progress: 0.5, owner: [{ resource_id: "10", value: 4 }, { resource_id: "9", value: 5 }], priority: 1 },
{ id: 9, text: "Workplaces importation", type: "task", start_date: "21-04-2025 00:00", duration: 4, parent: "4", progress: 0.5, owner: [{ resource_id: "7", value: 3 }] },
{ id: 10, text: "Workplaces exportation", type: "task", start_date: "27-04-2025 00:00", duration: 3, parent: "4", progress: 0.5, owner: [{ resource_id: "8", value: 5 }], priority: 2 },
{ id: 11, text: "Product launch", type: "project", progress: 0.6, start_date: "02-04-2025 00:00", duration: 13, owner: [{ resource_id: "5", value: 4 }], parent: 0 },
{ id: 12, text: "Perform Initial testing", type: "task", start_date: "03-04-2025 00:00", duration: 5, parent: "11", progress: 1, owner: [{ resource_id: "7", value: 6 }] },
{ id: 13, text: "Development", type: "project", start_date: "03-04-2025 00:00", duration: 11, parent: "11", progress: 0.5, owner: [{ resource_id: "5", value: 2 }] },
{ id: 14, text: "Analysis", type: "task", start_date: "03-04-2025 00:00", duration: 6, parent: "11", owner: [], progress: 0.8 },
{ id: 15, text: "Design", type: "project", start_date: "03-04-2025 00:00", duration: 5, parent: "11", progress: 0.2, owner: [{ resource_id: "5", value: 5 }] },
{ id: 16, text: "Documentation creation", type: "task", start_date: "03-04-2025 00:00", duration: 7, parent: "11", progress: 0, owner: [{ resource_id: "7", value: 2 }], priority: 1 },
{ id: 17, text: "Develop System", type: "task", start_date: "03-04-2025 00:00", duration: 2, parent: "13", progress: 1, owner: [{ resource_id: "8", value: 1 }], priority: 2 },
{ id: 25, text: "Beta Release", type: "milestone", start_date: "06-04-2025 00:00", parent: "13", progress: 0, owner: [{ resource_id: "5", value: 1 }], duration: 0 },
{ id: 18, text: "Integrate System", type: "task", start_date: "10-04-2025 00:00", duration: 2, parent: "13", progress: 0.8, owner: [{ resource_id: "6", value: 2 }], priority: 3 },
{ id: 19, text: "Test", type: "task", start_date: "13-04-2025 00:00", duration: 4, parent: "13", progress: 0.2, owner: [{ resource_id: "6", value: 3 }] },
{ id: 20, text: "Marketing", type: "task", start_date: "13-04-2025 00:00", duration: 4, parent: "13", progress: 0, owner: [{ resource_id: "8", value: 4 }], priority: 1 },
{ id: 21, text: "Design database", type: "task", start_date: "03-04-2025 00:00", duration: 4, parent: "15", progress: 0.5, owner: [{ resource_id: "6", value: 5 }] },
{ id: 22, text: "Software design", type: "task", start_date: "03-04-2025 00:00", duration: 4, parent: "15", progress: 0.1, owner: [{ resource_id: "8", value: 3 }], priority: 1 },
{ id: 23, text: "Interface setup", type: "task", start_date: "03-04-2025 00:00", duration: 5, parent: "15", progress: 0, owner: [{ resource_id: "8", value: 5 }], priority: 1 },
{ id: 24, text: "Release v1.0", type: "milestone", start_date: "20-04-2025 00:00", parent: "11", progress: 0, owner: [{ resource_id: "5", value: 3 }], duration: 0 }
],
links: [
{ id: "2", source: "2", target: "3", type: "0" },
{ id: "3", source: "3", target: "4", type: "0" },
{ id: "7", source: "8", target: "9", type: "0" },
{ id: "8", source: "9", target: "10", type: "0" },
{ id: "16", source: "17", target: "25", type: "0" },
{ id: "17", source: "18", target: "19", type: "0" },
{ id: "18", source: "19", target: "20", type: "0" },
{ id: "22", source: "13", target: "24", type: "0" },
{ id: "23", source: "25", target: "18", type: "0" }
],
// load resource values
resources: [
{ id: 1, text: "QA", parent: null },
{ id: 2, text: "Development", parent: null },
{ id: 3, text: "Sales", parent: null },
{ id: 4, text: "Other", parent: null },
{ id: 5, text: "Unassigned", parent: 4 },
{ id: 6, text: "John", parent: 1, unit: "hours/day" },
{ id: 7, text: "Mike", parent: 2, unit: "hours/day" },
{ id: 8, text: "Anna", parent: 2, unit: "hours/day" },
{ id: 9, text: "Bill", parent: 3, unit: "hours/day" },
{ id: 10, text: "Floe", parent: 3, unit: "hours/day" }
]
};
src/examples/resource-panel/ResourceSelectDropdown.tsx View on GitHub
import React, { useState, useEffect } from 'react';
import { FormControl, InputLabel, Select, MenuItem } from '@mui/material';
type ResourceSelectDropdownProps = {
resources: any[];
initialValue: string | null;
onChange: ((resourceId: string | null) => void);
};
export default function ResourceSelectDropdown({
resources,
initialValue,
onChange
}: ResourceSelectDropdownProps) {
const [selectedValue, setSelectedValue] = useState<string | null>(initialValue);
const handleChange = (e: any) => {
const value = e.target.value;
setSelectedValue(value);
if (value === 'all') {
onChange(null);
} else {
onChange(value);
}
};
return (
<FormControl variant="outlined" size="small">
<InputLabel id="resource-select-label">Resource</InputLabel>
<Select
labelId="resource-select-label"
label="Resource"
value={selectedValue || "all"}
onChange={handleChange}
style={{ minWidth: 200 }}
>
<MenuItem value="all">All Resources</MenuItem>
{resources.map((res) => (
<MenuItem value={String(res.id)} key={res.id}>
{res.text}
</MenuItem>
))}
</Select>
</FormControl>
);
}
src/examples/resource-panel/hooks/useResourceHistogram.ts View on GitHub
import { useMemo, useCallback } from 'react';
import { useGanttDatastore, useResourceAssignments, Task, ResourceItem} from '@dhtmlx/trial-react-gantt';
interface UseResourceHistogram {
getAllocatedValue: (tasks: any[], resource: any) => number;
getCapacity: (date: Date, resource: any) => number;
isGroupResource: (resource: any) => boolean;
}
export function useResourceHistogram(ganttRef: React.RefObject<any>): UseResourceHistogram {
const resourceStore = useGanttDatastore<any>(ganttRef, 'resource');
const { getResourceAssignments } = useResourceAssignments(ganttRef);
// provide your own logic for capacity
const WORK_DAY = 8;
const capacityCache = useMemo(() => new Map<string, number>(), []);
const isGroupResource = useCallback((resource: any) => {
return resourceStore.hasChild(resource.id);
}, [resourceStore]);
const getAllocatedValue = useCallback((tasks: Task[], resource: ResourceItem) => {
return tasks.reduce((sum: number, task: any) => {
const assignments = getResourceAssignments(resource.id, task.id);
return sum + assignments.reduce((acc: number, a: any) => acc + Number(a.value), 0);
}, 0);
}, [getResourceAssignments]);
const getCapacity = useCallback((date: Date, resource: any) => {
if (isGroupResource(resource)) {
return -1; // e.g. group resources have no capacity
}
const key = `${date.valueOf()}_${resource.id}`;
if (!capacityCache.has(key)) {
// simplistic random capacity logic
const randomFactor = [0, 1, 2, 3][Math.floor(Math.random() * 4)];
capacityCache.set(key, randomFactor * WORK_DAY);
}
return capacityCache.get(key)!;
}, [capacityCache, isGroupResource]);
return {
getAllocatedValue,
getCapacity,
isGroupResource
};
}
src/examples/resource-panel/styles.css View on GitHub
.gantt_grid_scale .gantt_grid_head_cell,
.gantt_task .gantt_task_scale .gantt_scale_cell {
font-weight: bold;
font-size: 14px;
}
.folder_row {
font-weight: bold;
}
.highlighted_resource,
.highlighted_resource.odd {
background-color: rgba(255, 251, 224, 0.6);
}
.gantt_task_cell.week_end {
background-color: #e8e8e87d;
}
.gantt_task_row.gantt_selected .gantt_task_cell.week_end {
background-color: #e8e8e87d !important;
}
.group_row,
.group_row.odd,
.gantt_task_row.group_row {
background-color: rgba(232, 232, 232, 0.6);
}
.owner-label {
width: 20px;
height: 20px;
line-height: 20px;
font-size: 12px;
display: inline-block;
border: 1px solid #cccccc;
border-radius: 25px;
background: #e6e6e6;
color: #6f6f6f;
margin: 0 3px;
font-weight: bold;
}
.resource-select-panel .gantt_layout_content {
line-height: 35px;
text-align: right;
padding: 8px;
overflow: hidden !important;
}
.resource-select {
padding: 3px;
margin-left: 13px;
}
.column_overload .gantt_histogram_fill {
background-color: color-mix(in srgb, var(--dhx-gantt-base-colors-error), transparent 60%);
}