Business hours and availability
Shade the time a resource cannot be booked, and choose whether the grid also stops people booking it.
Business hours
Section titled “Business hours”Pass businessHours to shade everything outside working time. Every field is optional:
<ResourceScheduler resources={resources} businessHours={{ daysOfWeek: [1, 2, 3, 4, 5], startHour: 9, endHour: 17 }}/>Those values are the defaults, so businessHours={{}} means Monday to Friday, 9 to 17. Without the prop nothing is shaded.
- In the day view, slots outside the hours and on non-working days are shaded.
- In the week view and longer, columns are whole days, so only non-working days are shaded. The hours do not apply.
Per resource
Section titled “Per resource”A resource can carry its own hours, which replace the shared ones, and its own blocked periods:
const resources: Resource[] = [ { id: "ann", name: "Ann", events: [], // Lunch, leave, an appointment: any period she cannot be booked. unavailable: [{ start: new Date(2026, 9, 7, 12), end: new Date(2026, 9, 7, 13) }], }, { id: "bob", name: "Bob", events: [], businessHours: { startHour: 12, endHour: 20 } }, // late shift { id: "chen", name: "Chen", events: [], businessHours: false }, // always available];unavailable ranges have an exclusive end. In the day view a slot is shaded when a range overlaps it at all. In the date views a day is shaded only when a range covers all of it, so a morning appointment does not grey out the whole day.
Stopping bookings
Section titled “Stopping bookings”Shading is only visual by default: people can still create and drop events there. Add blockUnavailable and every move, resize and create that touches a shaded slot is rejected, with the same red outline as a rejected overlap:
<ResourceScheduler resources={resources} businessHours={{}} blockUnavailable eventOverlap={false}/>It runs together with eventOverlap and isValidDrop. In the date views an event that spans a non-working day, such as Friday to Monday, touches that day and is rejected. If you need long events across weekends, leave blockUnavailable off and use isValidDrop with the helpers below.
Keyboard users and screen readers get the same information: a shaded cell is announced as “unavailable”, and rejected placements read “Not allowed”.
Find a free slot
Section titled “Find a free slot”findAvailableSlots answers “when is this person free for an hour?” using the same rules the grid shows: the resource’s events, its unavailable time, and business hours.
import { findAvailableSlots } from "resource-scheduler";
const slots = findAvailableSlots(ann, { from: new Date(2026, 9, 7, 8), to: new Date(2026, 9, 9, 18), // exclusive: a slot must end by then duration: 60, // minutes step: 30, // candidate starts every 30 minutes (default) businessHours: {}, // Monday to Friday, 9 to 17 limit: 5, // optional});// [{ start, end }, ...] earliest firstPass an array of resources to find a time when all of them are free, which is the meeting-scheduling case:
const common = findAvailableSlots([ann, bob, room], { from, to, duration: 30 });Results start on the step grid and can overlap each other (9:00 and 9:30 are both offered for a one hour slot). If your events live in a flat list, merge them first with withEvents(resources, events).
Suggest who takes a job
Section titled “Suggest who takes a job”findAvailableSlots finds when a resource is free. suggestResources answers the dispatcher’s question: who should get this job? It asks every resource, applies the rules the grid enforces, and returns the placements that work, best first.
import { suggestResources, withEvents } from "resource-scheduler";
const [best, ...others] = suggestResources( withEvents(technicians, jobs), { from: new Date(2026, 9, 7, 7), to: new Date(2026, 9, 7, 19), duration: 90 }, { strategy: "least-booked", limit: 3 });// best: { resourceId: "van", start, end, score: 0.25, utilization: 0.25 }Give it a window to search and each resource gets its free slots (step works as in findAvailableSlots, 30 by default), one per resource unless you raise perResource. Give it a fixed time instead, { start, end }, and it lists the resources that can take exactly that. The fixed form is also the check to run when somebody drops a job, as the demo does:
<ResourceScheduler resources={resources} isValidDrop={(job, { resourceId, start, end }) => suggestResources(resources, { start, end, event: job }, { resourceIds: [resourceId] }).length > 0 }/>event is the job being placed. The rules receive it, and a copy of it already on a resource does not count against that resource, so the same call works for moving a job that is booked.
A placement is suggested only when the resource:
- is inside its hours (its own
businessHours, else the sharedbusinessHoursoption) and misses itsunavailableranges; - has fewer bookings running at once than its
capacity(default 1), recurring events included. With a capacity of 2, one booking leaves a seat and two do not. Bookings that only touch, 9 to 10 and 10 to 11, never run at once. The grid does not enforcecapacityon its own, so this is where it counts. A capacity of 0 is never suggested; - passes
eventOverlapandisValidDrop, called as the props are (this isisPlacementAllowed), so it never suggests what the board would refuse.
Ranking
Section titled “Ranking”strategy |
Ranks by | Use it to |
|---|---|---|
"earliest" (default) |
the soonest start | get the job done first |
"least-booked" |
the lowest utilization | spread work out |
"most-booked" |
the highest utilization | fill the busiest resource first |
| a function | the number you return for { resource, start, end, utilization } |
rank by skills, distance or cost |
Lowest score comes first. utilization is what getUtilization gives for the window you searched (the day of a fixed time) before the job, the same share showUtilization draws. Ties keep the order of resources, then the earlier start, so the same data always gives the same answer. A function that returns NaN or Infinity drops the candidate:
suggestResources(resources, request, { // Only technicians certified for gas work, soonest first. strategy: ({ resource, start }) => (skills[resource.id].includes("gas") ? start.getTime() : Infinity),});The other options are businessHours, resourceIds (look at these resources only), limit and perResource (Infinity lists every free slot instead of the best one).
Time zones. Like findAvailableSlots, it reads hours and days on the clock of the Dates you pass. With the timeZone prop your stored events and unavailable ranges are instants, and isValidDrop receives instants. To search inside a zone’s working hours, convert the events and ranges with utcToZoned, search, and convert the start and end of the answers back with zonedToUtc.
Limits. With a built-in strategy and no eventOverlap or isValidDrop rule it stops at the first free slots of each resource: 1,000 resources over a week take about 60 ms. With a score function or a rule it checks every candidate slot in the window, about 1.5 s for the same 1,000 resources, so keep the window short and use resourceIds for long lists. It does not move other events to make room, and it does not split a job across resources.
The helpers
Section titled “The helpers”The same checks are exported, so you can run them on a server:
import { isCellUnavailable, touchesUnavailable } from "resource-scheduler";
touchesUnavailable(resource, { start, end }, { businessHours, viewType, slotMinutes });Now indicator
Section titled “Now indicator”nowIndicator draws a line at the current time in today’s day view and moves it every minute. It is hidden on other days, outside the visible hours, and in the other views. Off by default.
<ResourceScheduler resources={resources} initialView={ViewType.Day} nowIndicator />