When an administrator opens a scheduling application at 6 AM, they see a deceptively simple interface: a grid of days, each divided into time slots, scattered with colored dots representing shifts. Behind that simplicity lives a system that must answer a single question hundreds of times per second: who is working, where, and when?
The schedule is the beating heart of any staffing application. It is the surface through which facilities post shifts, workers get booked, and the entire staffing lifecycle plays out. It ships as two views — the Daily View and the Monthly View — each optimized for a different job.
The Daily View
The Daily View answers the question: “What is happening today?” It shows every shift for a single day, grouped into three columns — one for each time slot. On desktop, a collapsible sidebar provides a mini calendar and qualification filters. On mobile, the same information unfolds as a horizontal carousel with bottom sheets.
This responsive split isn’t a CSS media query trick. The daily view ships two entirely separate component trees: DesktopDailyView and MobileDailyView, chosen at the container level based on a 750px viewport threshold. Each is purpose-built for its form factor.
Time slots
Healthcare staffing revolves around three shifts. We call them time slots — AM, PM, and NOC (nocturnal). Every shift posted on the platform falls into one of these three buckets based on its start hour.
| Slot | Hours | Start Range | Typical Role |
|---|---|---|---|
| AM | 05:00 – 11:59 | Morning | Day shift nurses, CNAs |
| PM | 12:00 – 17:59 | Afternoon | Evening coverage |
| NOC | 18:00 – 04:59 | Night | Overnight staff |
The function getTimeSlotFromHour() is the single source of truth for this mapping. When a shift arrives from the API with a start time of 2026-03-04T14:00:00Z, we extract the hour in the facility’s timezone, and that hour determines the column.
Why timezone matters here: A shift starting at
00:00 UTCcould be an evening PM shift in California (4 PM PST) or an early AM shift in London. The slot assignment always happens in the facility’s local timezone, never UTC.
How data flows
The daily view’s data is orchestrated by a single hook: useDailyViewData(). Think of it as a conductor coordinating five parallel API calls, each returning a different piece of the puzzle.
The two core API endpoints are /calendar/facility for full shift objects and /calendar/facilityCount for lightweight summaries. The count endpoint is intentionally fast — under 50ms at p99 — because it powers the calendar dot indicators that need to feel instant.
Qualification filtering works through a subtle pattern: rather than sending a list of selected qualifications in one request, we fire a separate API call for each selected type and merge the results. This guarantees accurate counts regardless of how the backend handles multi-type queries.
// Each selected qualification triggers its own query
const queries = selectedTypes.map(type =>
createFacilityShiftCountsQueryOptions({
facilityId,
dateFilter: { start, end },
tmz: facility.timezone,
type: type.name,
})
);
// Results are merged after all queries resolve
const merged = combineShiftCountResults(results);
The Monthly View
If the Daily View is a microscope, the Monthly View is a telescope. It shows the entire month at a glance — a 7-column by 6-row grid of 42 day cells, including padding days from adjacent months. Each cell contains three stacked time slots with dot indicators showing shift presence and fill state.
The Monthly View introduces two posting modes: Bulk Posting and Block Booking. Bulk posting adds incremental shifts across multiple days, while block booking reserves contiguous slot ranges for specific workers. Both use localStorage-backed draft persistence with schema versioning, so accidentally closing the tab doesn’t lose 10 minutes of work.
Making navigation feel instant
The schedule uses an aggressive prefetching strategy. When you’re looking at March 4th, we’re already fetching March 3rd and March 5th in the background. When you hover over the “Month” tab, we start loading the monthly view data before you click.
View transitions between day and month use the CSS view-transition-name API. Combined with prefetched data, the switch feels like flipping a page rather than loading a new one.
function startScheduleViewTransition({ update }) {
if (!document.startViewTransition) {
update();
return;
}
document.startViewTransition(() => {
flushSync(() => update());
});
}
Every view is a permalink
All navigation state lives in the URL: ?date=2026-03-04&view=day&shiftId=abc123. This means every view is a permalink. You can share a link to a specific shift on a specific day, and the recipient sees exactly what you see. The useShiftDeepLinking() hook parses these parameters on mount and auto-opens the relevant drawers and dialogs. All URL updates use history.replace() rather than push(), so the browser back button takes you out of the schedule entirely rather than cycling through every shift you looked at.