Frontend at ZF SCALAR

Not long after leaving Archilogic, I talked to David Geretti with whom I worked at Bestmile. He suggested I could come back work on the project that came out of the acquisition of Bestmile by ZF SCALAR. I would have the opportunity to collaborate again with some of the most competent colleagues ever (especially Jean-Baptiste Cochery), while helping evolve the great fleet orchestration software developed at Bestmile.

My contribution was threefold:

Velocity design system

My first task was to implement some of the React components for the new design system codenamed “Velocity”. To be honest, it was nothing super exciting, but it allowed me to get familiar with the chosen frameworks and technologies, which then made me more efficient in using the new design system. We used Adobe React Spectrum, pure CSS (no SCSS), and some Figma design token extraction system to build components, all documented in Storybook. For the first time in my career, we built a design system with support for a light and a dark color scheme out of the gate.

In 2 months, I built React components for tabs, link, select, multi-select, divider, menu, popover, tag, avatar, badge, radiogroup, and did substantial work on button, switch, radio, checkbox, form. Basically laying the groundwork to be able to actually use the new design system in an existing app, which would be my next task.

The Badge, one of the more fun components to implement
The Badge, one of the more fun components to implement

I also helped structure the documentation a bit better, added a pre-commit hook to enforce a commit message syntax and run tests. And, as always, found and fixed tons of bugs due to a low test coverage.

People Mobility

Exactly 3 years after leaving Bestmile, I was again working on the UI for fleet orchestration. This was now the fourth iteration. The first was developed prior to me joining Bestmile, the second was the one I worked on, the third one basically was a partial migration to ZF, and now this fourth one would be yet another migration, replacing entirely what was done at Bestmile.

Here is what the tech stack of the People Mobility web app looks like:

You can see a lot of well established tech, libraries and frameworks there. Which is exactly how I like it. It is so good to have everything in TypeScript. Even the auto-generated API clients and types. Faker JS and Mock Service Worker are 2 awesome tools that were new to me, and that allow to write great unit tests.

At the app level, besides my usual pet peeves like good unit test coverage, a decent code consistency, proper documentation, I got to:

|                               | Dispatcher | Booking Agent | Cargo Planner |
| ----------------------------- | ---------- | ------------- | ------------- |
| PeopleMobility app            | ╳          | ╳             |               |
| Bookings module               | ╳          | ╳             |               |
| Dispatching module            | ╳          | ╳             |               |
| Scheduling module             | ╳          |               |               |
| Services module               | ╳          |               |               |
| Travelers module              | ╳          | ╳             |               |
| Planning module               |            |               | ╳             |
| TravelerAnnouncements feature | ╳          |               |               |
User permissions summary, as generated by unit tests
Example of unit tests output, phrased like specifications
Services > renders a list of active services
Services > when a service is selected > renders a "General" tab (selected by default) with a form
Services > when a service is selected > then the general properties are changed > shows a changelog
Services > when a service is selected > then the general properties are changed > then the change is submitted and the call fails > shows an error
Services > when a service is selected > then the general properties are changed > then the change is submitted and the call succeeds > sends the correct data to the server
Services > when a service is selected > then the "Service hours" tab is selected > renders a form
Services > when a service is selected > then the "Service hours" tab is selected > then the weekly-recurring service hours are changed > shows a changelog
Services > when a service is selected > then the "Service hours" tab is selected > then the weekly-recurring service hours are changed > then the change is submitted > sends the correct data to the server
Services > when a service is selected > then the "Service hours" tab is selected > then service hours are changed to be daily-recurring > shows a changelog
Services > when a service is selected > then the "Service hours" tab is selected > then service hours are changed to be daily-recurring > then the change is submitted > sends the correct data to the server
Services > when a service is selected > then the "Service hours" tab is selected > then service hours are changed to be non-stop > shows a changelog
Services > when a service is selected > then the "Service hours" tab is selected > then service hours are changed to be non-stop > then the change is submitted > sends the correct data to the server
Services > when a service is selected > then the "Service area" tab is selected and the form expanded > renders a form and a map
Services > when a service is selected > then the "Service area" tab is selected and the form expanded > then service area is changed > shows a changelog
Services > when a service is selected > then the "Service area" tab is selected and the form expanded > then service area is changed > then the change is submitted > sends the correct data to the server
Services > when a service is selected > then the "Dispatching parameters" tab is selected > renders a form
Services > when a service is selected > then the "Dispatching parameters" tab is selected > then the dispatching parameters are changed > shows a changelog
Services > when a service is selected > then the "Dispatching parameters" tab is selected > then the dispatching parameters are changed > then the change is submitted > sends the correct data to the server
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > renders a form, a paginated table, and a map with a places layer
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > then the maximum walking distance is changed > shows a changelog
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > then the maximum walking distance is changed > then the change is submitted > sends the correct data to the server
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > then a row is unselected > shows a changelog
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > then a row is unselected > then the change is submitted > calls the right endpoint
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > then a row is selected > shows a changelog
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > then a row is selected > then the change is submitted > calls the right endpoint
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > then a query is typed into the places filter > filters the table
Services > when a service is selected > then the "Service stops" tab is selected and the form expanded > then a row in the table is clicked > shows a marker on the map
Services > when a service is selected > then the "Pricing configuration" tab is selected > renders a form
Services > when a service is selected > then the "Pricing configuration" tab is selected > then the pricing parameters are changed > shows a changelog
Services > when a service is selected > then the "Pricing configuration" tab is selected > then the pricing parameters are changed > then the change is submitted > sends the correct data to the server
Services > when a coordinate-based service is selected > does not render a "Service stops" tab
Services > when a station-based service is selected > renders a "Service stops" tab
Services > when a disabled service is activated > shows a confirmation dialog
Services > when an active service is disabled > shows a confirmation dialog
Services > when the button to create a new service is clicked and the form is filled > then the form is submitted and the call fails > shows an error
Services > when the button to create a new service is clicked and the form is filled > then the form is submitted and the call succeeds > sends the correct data to the server
Services > when the organization has no pricing models and a service is selected > does not render a "Pricing configuration" tab
Services > when the organization has pricing models and a service is selected > renders a "Pricing configuration" tab
Bookings > displays a loader, then a bookings table, and a button to create a booking
Bookings > when a row is expanded, and the booking has no ride and no traveler > displays the booking details
Bookings > when a row is expanded, and the booking has a ride > displays the ride details
Bookings > when a row is expanded, and the booking has a traveler > displays the traveler details
Bookings > when the search by traveler name is used > applies the search
Bookings > when the search by vehicle name is used > applies the search
Bookings > when the filters are used > applies the filters
Bookings > when the booking ID filter is used > disables the date range filter and applies the booking ID filter
Bookings > when there is a booking ID filter in the URL > disables the date range filter and applies the booking ID filter
Bookings > when the column filter is used > applies the column filter
Bookings > then a ride rematch is requested, and it succeeds > shows a confirmation
Bookings > then a ride rematch is requested, and it fails > shows a dialog
Bookings > then a ride rematch is requested, and it fails > then the booking is canceled, and the API call fails > shows an error
BookingCreation > when no fare options are returned > displays the first step of the booking creation funnel
BookingCreation > when no fare options are returned > then an existing traveler is selected > pre-fills the traveler form
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > displays a warning
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > displays validation errors
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > displays the second step of the booking creation funnel
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > then the second step is completed by searching for an origin and a destination, but the quote request fails > shows the errors
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > then the second step is completed by searching for an origin and a destination, and by entering a desired journey start time, as well as specifying custom time deviations and not overriding service hours > creates a quote and displays the third step of the booking creation funnel
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > then the second step is completed by searching for an origin and a destination, and by entering a desired journey start time, as well as specifying custom time deviations and not overriding service hours > then a service is selected > then the booking succeeds > displays a booking confirmation
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > then the second step is completed by searching for an origin and a destination, and by entering a desired journey start time, as well as specifying custom time deviations and not overriding service hours > then a service is selected > then the booking succeeds > then the user starts over > displays the first step of the booking creation funnel
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > then the second step is completed by searching for an origin and a destination, and by entering a desired journey start time, as well as specifying custom time deviations and not overriding service hours > then a service is selected > then the booking fails > displays a booking rejection
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > then the second step is completed by searching for an origin and a destination, and by entering a desired journey start time, as well as specifying custom time deviations and not overriding service hours > then the user backtracks > displays the second step of the booking creation funnel
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > displays the second step of the booking creation funnel with the traveler’s recent booking(s)
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > then the second step is completed by selecting a recent booking > creates a quote and displays the third step of the booking creation funnel
BookingCreation > when no fare options are returned > then an existing traveler is selected > then the form is modified > then the option to create a new traveler is selected and the form is submitted > then the form is correctly filled, a wheelchair passenger is added, and the form is submitted > then the user backtracks > displays the first step of the booking creation funnel, still filled
BookingCreation > when fare options are returned > then an existing traveler is selected, fare options are selected, a wheelchair space is requested, and the first step is submitted > then the second step is completed by searching for an origin and a destination > creates a quote and displays the third step of the booking creation funnel

For several months, a background task of mine was to migrate the whole UI to the new design system. This seems to be a recurring theme in my job.

Migration to the new design system (old one on the left, new one on the right). One of the main goals was to have a denser UI
Migration to the new design system (old one on the left, new one on the right). One of the main goals was to have a denser UI
Comparison between the navigation in other SCALAR apps, and in the People Mobility app before and after the migration to the new design system
Comparison between the navigation in other SCALAR apps, and in the People Mobility app before and after the migration to the new design system

Bookings

One of the first features I had to work on was the booking creation funnel. Compared to the Bestmile version, it had support for wheelchair booking, configurable fare options (adult, child, dog, etc.), the selection of a past booking, etc.

Passengers selection based on configurable fare options
Passengers selection based on configurable fare options
Ability to re-use a traveler’s recent booking for the journey’s origin and destination
Ability to re-use a traveler’s recent booking for the journey’s origin and destination
Preview of a possible route for a station-based service, involving walking to/from the stops
Preview of a possible route for a station-based service, involving walking to/from the stops

The whole booking creation funnel layout was made responsive (map on top for narrow screens, instead of to the right), and using the new design system gave a dark theme for free.

Responsive layout and dark theme
Responsive layout and dark theme

Some version of the bookings list already existed when I joined, and I got to add features like ride rematch (e.g. for incident management), or ride flagging.

Bookings list with booking details, and actions like ride flagging, ride rematch, and ride cancellation
Bookings list with booking details, and actions like ride flagging, ride rematch, and ride cancellation

Traveler management

Another feature that existed already was listing and editing travelers. I just added the ability to delete a traveler.

Traveler management, nothing fancy
Traveler management, nothing fancy

Bestmile had a simple traveler announcements system to display messages to users of the (ride hailing) traveler apps. I ported that feature to the People Mobility app.

Traveler announcements, very straightforward CRUD feature
Traveler announcements, very straightforward CRUD feature

Service configuration

The next re-implementation of Bestmile features was for service configuration. I had a lot of fun creating SVG graphs for the explanations of dispatching parameters. I wanted to make the graphs dynamically adapt to the values the user enters, but nobody liked it. The code would still allow it.

Hand-crafted SVG graphics to explain dispatching parameters
Hand-crafted SVG graphics to explain dispatching parameters
The SVG-generating code would have allowed for dynamic illustrations, parametrized by the user’s input
The SVG-generating code would have allowed for dynamic illustrations, parametrized by the user’s input

Another fun but tricky thing was the three-way percentage split of the optimization focus weights. Changing one value influences all other values, unless they are locked.

Optimization focus weights
Optimization focus weights
A user interaction that is tricky to get right
A user interaction that is tricky to get right
The three-way percentage split code and tests
export function threeWayPercentageSplit(
  a: { value: number; newValue: number },
  b: { value: number; locked: boolean },
  c: { value: number; locked: boolean },
): { a: number; b: number; c: number } {
  if (!b.locked && !c.locked) {
    const delta = a.value - a.newValue;
    if (c.value > b.value) {
      const newB = Math.max(0, Math.min(b.value + delta / 2, 100));
      return {
        a: a.newValue,
        b: newB,
        c: 100 - a.newValue - newB,
      };
    } else {
      const newC = Math.max(0, Math.min(c.value + delta / 2, 100));
      return {
        a: a.newValue,
        b: 100 - a.newValue - newC,
        c: newC,
      };
    }
  }

  if (!b.locked) {
    const limitedNewA = Math.min(a.newValue, 100 - c.value);
    return {
      a: limitedNewA,
      b: 100 - limitedNewA - c.value,
      c: c.value,
    };
  }

  if (!c.locked) {
    const limitedNewA = Math.min(a.newValue, 100 - b.value);
    return {
      a: limitedNewA,
      b: b.value,
      c: 100 - limitedNewA - b.value,
    };
  }

  return { a: a.value, b: b.value, c: c.value };
}
describe('threeWayPercentageSplit', () => {
  describe('when A is changed, B and C are locked', () => {
    it('ignores the change', () => {
      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 34 },
          { value: 56, locked: true },
          { value: 32, locked: true },
        ),
      ).toEqual({ a: 12, b: 56, c: 32 });
    });
  });

  describe('when A is changed, B is not locked, but C is', () => {
    it('distributes the change to B', () => {
      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 34 },
          { value: 56, locked: false },
          { value: 32, locked: true },
        ),
      ).toEqual({ a: 34, b: 34, c: 32 });

      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 0 },
          { value: 56, locked: false },
          { value: 32, locked: true },
        ),
      ).toEqual({ a: 0, b: 68, c: 32 });

      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 100 },
          { value: 56, locked: false },
          { value: 32, locked: true },
        ),
      ).toEqual({ a: 68, b: 0, c: 32 });
    });
  });

  describe('when A is changed, B is locked, C is not', () => {
    it('distributes the change to C', () => {
      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 34 },
          { value: 56, locked: true },
          { value: 32, locked: false },
        ),
      ).toEqual({ a: 34, b: 56, c: 10 });

      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 0 },
          { value: 56, locked: true },
          { value: 32, locked: false },
        ),
      ).toEqual({ a: 0, b: 56, c: 44 });

      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 100 },
          { value: 56, locked: true },
          { value: 32, locked: false },
        ),
      ).toEqual({ a: 44, b: 56, c: 0 });
    });
  });

  describe('when A is changed, neither B nor C are locked', () => {
    it('distributes the change to B and C', () => {
      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 34 },
          { value: 56, locked: false },
          { value: 32, locked: false },
        ),
      ).toEqual({ a: 34, b: 45, c: 21 });

      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 0 },
          { value: 56, locked: false },
          { value: 32, locked: false },
        ),
      ).toEqual({ a: 0, b: 62, c: 38 });

      expect(
        threeWayPercentageSplit(
          { value: 12, newValue: 100 },
          { value: 56, locked: false },
          { value: 32, locked: false },
        ),
      ).toEqual({ a: 100, b: 0, c: 0 });
    });
  });
});

One last noteworthy implementation detail for the service configuration part of the People Mobility app are the service hours. You can specify slots when a passenger transportation service is available. That was a perfect use case for React Hook Form’s useFieldArray.

const { fields, append, remove, insert, update } = useFieldArray({
  control,
  name: 'hours',
});
Custom schedule for service hours, powered by React Hook Form’s useFieldArray
Custom schedule for service hours, powered by React Hook Form’s useFieldArray

Live operations

A basic version of the “Dispatching” screen already existed, but I worked on many improvements like:

The very information-dense live dispatching screen
The very information-dense live dispatching screen
Resizable panels, with min and max dimensions, and current dimension saved in user preferences
Resizable panels, with min and max dimensions, and current dimension saved in user preferences

Shift planning

I was a bit less involved in the “Shift planning” screen, but got to touch pretty much every corner of the codebase, either by fixing bugs, adding tests or migrating to the new design system.

Shift planning, with driver assignment and vehicle mission details
Shift planning, with driver assignment and vehicle mission details
Vehicle shift details
Vehicle shift details

At some point I did a prototype for drag’n’dropping vehicle shifts, with a “drag ghost element” that would indicate with green/red colors where you can drop the shift. This never made it into the final product, but the experiment gave some good insights for the upcoming Cargo Planning app.

Drag’n’drop prototype
Drag’n’drop prototype

Cargo Planning

While I was finishing to migrate Bestmile features to the People Mobility app, the rest of the team started to work on a proof of concept for Cargo Planning. I soon joined the effort and worked on:

Assign customer orders (a.k.a. unassigned trips) to a driver-truck-trailer resource combo, either by drag’n’drop onto the timeline, or by using the form. All while getting planning assistance with trip chaining opportunities
Assign customer orders (a.k.a. unassigned trips) to a driver-truck-trailer resource combo, either by drag’n’drop onto the timeline, or by using the form. All while getting planning assistance with trip chaining opportunities
Filter trips ending within a given distance of the start of the trip the user is trying to plan, to favor trip chaining for an optimal resources use
Filter trips ending within a given distance of the start of the trip the user is trying to plan, to favor trip chaining for an optimal resources use
Turning the unassigned trips list into a table when the left panel is wide enough
Turning the unassigned trips list into a table when the left panel is wide enough

It was fun to work on this prototype and quickly iterate on ideas. Being able to fake the API calls allowed to move fast without needing to wait on anybody else.

Also, I could use container queries for the first time!

Container queries allowing to show the order details either in one column (on the left), or in 2 columns (on the bottom), depending on the width of the container element
Container queries allowing to show the order details either in one column (on the left), or in 2 columns (on the bottom), depending on the width of the container element
<div style={{ containerType: 'inline-size' }}></div>
.orderDetails {
  grid-template-columns: 1fr;
  grid-template-rows: repeat(2, auto);
}
@container (width >= 400px) {
  .orderDetails {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: 1fr;
  }
}

Conclusion

This is the second job in a row where I could focus solely on frontend development. Some of the new, or new to me at least, things I used are:

I only recently started to dabble with AI via Claude Code and Copilot. I can see how those tools can be useful for:

Overall I think that all the AI tools are still in their early stages, and quite experimental. I’m curious to see how they will evolve, and what will remain after the AI bubble has burst.