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.

Input2026-10-23Date-only day off
Incorrect interpretationOct 22UTC midnight formatted in Los Angeles
Expected resultOct 23Preserve 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 exampleNot 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 callnew 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 CONTRACTSShifts · overtimeIdentifiers and time fields
SERVICE CONTRACTSAbsences · days offCategories and visibility rules
Typed application event modelStable identity · date handling · categories · permission metadata
Product rulesNormalized events and consistent access rules
View adaptersDay · week · month · team
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.

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
01Week 42Load the visible range
02Week 43Navigate forward
03Week 42Reuse 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.