In a workforce calendar, shifts, absences, and days off can look like variations of the same event. They aren't. A day off belongs to a calendar date; a shift belongs to a span of time. Mixing those rules can move an event to the wrong day without producing an obvious error.
I worked on a workforce calendar using React, TypeScript, and Bryntum. It combined shifts, absences, overtime, and days off from separate services. They looked similar in the calendar, but their date handling and visibility rules were not interchangeable.
A day off that moves to yesterday
Take a day-off record with the value 2026-10-23. Parsing that string as midnight UTC and displaying it in Los Angeles can produce October 22. The browser has converted an instant correctly; the mistake is that a day off was never meant to be an instant.
| Input | Incorrect interpretation | Expected result |
|---|---|---|
2026-10-23 — date-only day off |
Oct 22 — UTC midnight formatted in Los Angeles | Oct 23 — preserve the intended calendar day |
A shift timestamp is different: it represents a moment that may be displayed at different local times. We kept those two paths separate. Here's a reduced TypeScript illustration of that boundary:
TypeScript · illustrative example (not production code)
type CalendarRecord =
| { kind: "dayOff"; date: string }
| { kind: "shift"; startsAt: string };
function toCalendarEvent(record: CalendarRecord) {
if (record.kind === "dayOff") {
// Keep the calendar date as a date, not a UTC timestamp.
return { kind: "dayOff", calendarDate: record.date };
}
// A timestamp represents an instant; localize at display time.
return { kind: "shift", startsAt: record.startsAt };
}
The important choice is that the date-only branch does not call new Date() or silently apply timezone conversion. In a real application, the downstream rendering adapter also needs to preserve that meaning. I would test the same record across timezones and calendar views, not just test the conversion function in isolation.
The boundary before Bryntum
The services weren't returning one shared event format. We could have mapped each response directly into the calendar, then handled the differences in individual views. I chose a typed application event model instead. It gave IDs, categories, date handling, and permission metadata a consistent shape before anything reached Bryntum.
Where service-specific data stops and calendar rendering begins
SERVICE CONTRACTS
Shifts · overtime Absences · days off
Identifiers and time fields Categories and visibility rules
↓
Typed application event model
Stable identity · date handling · categories
Permission metadata
↓
Product rules View adapters
Normalized events Day · week · month · team
Consistent access rules
↓
Bryntum: event layout, interactions, and presentation
The extra mapping layer meant more code and more tests. The alternative was to maintain the same conversion and visibility logic in several places, including day, week, month, and team views. I preferred one boundary where the behavior could be reviewed and tested.
A responsive calendar still needs fresh data
Calendar navigation creates an awkward balance. People move between weeks and return to ranges they recently viewed. Fetching everything again makes the UI feel slow; never refreshing cached results can leave an old schedule on screen.
Example: moving across weeks, then returning
| Step | Range | Action |
|---|---|---|
| 01 | Week 42 | Load the visible range |
| 02 | Week 43 | Navigate forward |
| 03 | Week 42 | Reuse cached results; revalidate |
We reused loaded ranges, debounced quick navigation, and refreshed in the background when needed. The selected date and view could be restored, but the schedule itself still needed revalidation. Another important check was ensuring a late response from an earlier range could not overwrite the one currently on screen.
What I checked beyond the happy path
A populated calendar isn't proof that its data is right. The cases worth checking were the quiet failures: date-only records shifting days, confidential absence details showing in another view, cached ranges not refreshing, and an old request returning after the user had navigated somewhere else.
What I would carry into the next scheduling project is not a particular calendar configuration. It's the decision to make dates, permissions, and data freshness explicit before rendering. That gives the UI library less to guess about, and gives the rest of the application rules that can be tested without opening a calendar.
Originally published on nehamangla.com.Explore more of my engineering work and articles on my portfolio.
Top comments (0)