Grouping
Use resourceGrouping for team, department, location, room-building, machine-line, or sprint-team grouping. Parent-child resources can render as a tree.
grid.eventSchedulerResources = [ { id: 'north', name: 'North Campus', role: 'Location' }, { id: 'room-a', parentId: 'north', name: 'Room A', role: 'Room', metadata: { floor: '1' } }, { id: 'room-b', parentId: 'north', name: 'Room B', role: 'Room', metadata: { floor: '2' } },];
grid.eventScheduler = { view: 'resourceTimeline', weekStartDate: '2026-06-08', resourceGrouping: { enabled: true, tree: true, groupBy: (resource) => String(resource.metadata?.floor ?? resource.group ?? 'Other'), groupSummaryFormatter: ({ summary }) => `${summary.eventCount} events - ${summary.totalHours}h`, },};Group rows aggregate workload, coverage, and utilization summaries when those features are enabled.
Resource Planner Patterns
Section titled “Resource Planner Patterns”Use resourceTimeline when resources are the main rows and time moves horizontally.
grid.eventScheduler = { view: 'resourceTimeline', weekStartDate: '2026-06-08', dateRange: { start: '2026-06-08', end: '2026-06-14' }, resourceColumnSize: 220, timelineColumnSize: 72, resourceGrouping: { enabled: true, groupBy: (resource) => resource.metadata?.location as string, tree: true, collapsed: false, groupSummaryFormatter: ({ summary }) => `${summary.eventCount} events - ${summary.totalHours}h`, }, resourceMetaFormatter: (resource) => [resource.role, resource.metadata?.location].filter(Boolean).join(' - '),};Events without resourceId, resourceIds, or side-channel assignments render in the generated unassigned row when unassigned.enabled is not false.
grid.eventScheduler = { view: 'resourceTimeline', weekStartDate: '2026-06-08', unassigned: { enabled: true, showCount: true, requiredRoleField: 'metadata.requiredRole', filter: { status: ['draft', 'planned'], role: 'Technician' }, counterFormatter: (summary) => `${summary.visibleCount} open requests`, requiredRoleFormatter: ({ role }) => `Need ${role}`, assignmentValidator: ({ event, resource }) => { const required = event.metadata?.requiredRole; const skills = Array.isArray(resource.metadata?.skills) ? resource.metadata.skills : []; return !required || skills.includes(required) || `${resource.name} cannot fill ${required}.`; }, },};Coverage compares required capacity with assigned event capacity in the visible range. Utilization calculates scheduled workload by resource.
grid.eventScheduler = { view: 'resourceTimeline', weekStartDate: '2026-06-08', coverage: { enabled: true, capacityField: 'capacity', countEvent: ({ event, resource }) => event.status === 'cancelled' ? false : resource.capacity ?? 1, }, utilization: { enabled: true, minHoursField: 'metadata.minHoursPerWeek', targetHoursField: 'metadata.targetHoursPerWeek', maxHoursField: 'metadata.maxHoursPerWeek', excludeStatuses: ['cancelled', 'blocked'], },};Use filters and savedViews to reduce projected data without mutating source arrays:
grid.eventScheduler = { view: 'resourceTimeline', weekStartDate: '2026-06-08', filters: { search: 'security', resourceRoles: 'Security', eventStatuses: ['planned', 'confirmed'], conflictsOnly: false, unassignedOnly: false, }, activeSavedViewId: 'security-week', savedViews: { views: [ { id: 'conflicts', label: 'Conflicts only', filters: { conflictsOnly: true } }, { id: 'unassigned', label: 'Unassigned', filters: { unassignedOnly: true } }, { id: 'security-week', label: 'Security team', dateRange: { start: '2026-06-08', end: '2026-06-12' }, filters: { resourceRoles: 'Security' }, }, ], },};The projection exposes filterSummary with visible and hidden resource/event counts.