← Manuel Hitz, CV

ZF SCALAR

Senior Frontend Engineer · Jul 2024 – Apr 2026

Shortly after leaving Archilogic, I caught up with David Geretti, whom I had worked with at Bestmile. He suggested I join the team building on what came out of the acquisition of Bestmile by ZF SCALAR. It was a chance to work again with some of the most capable colleagues I have known, Jean-Baptiste Cochery in particular, and to keep evolving the fleet orchestration software we had built at Bestmile.

Velocity design system

My first assignment was to implement React components for the new design system, code-named “Velocity”. It was not the most exciting work, but it got me up to speed with the chosen frameworks, which made me much more effective once I started using the design system. We built on Adobe React Spectrum with plain CSS (no Sass), extracted design tokens from Figma, and documented everything in Storybook. For the first time in my career, the design system supported both a light and a dark theme from day one.

In two months, I built the tabs, link, select, multi-select, divider, menu, popover, tag, avatar, badge and radio group components, and did substantial work on button, switch, radio, checkbox and form. That laid the groundwork for using the design system in an existing app, which was my next task.

Badge component variants
The badge, one of the more fun components to implement

I also restructured parts of the documentation, added a pre-commit hook that runs the tests and enforces a commit message convention, and, as usual with low test coverage, found and fixed plenty of bugs.

People Mobility

Exactly three years after leaving Bestmile, I was back working on a fleet orchestration UI. This was its fourth iteration: the first predated my time at Bestmile, the second was the one I helped build there, the third was a partial migration to ZF, and this fourth one would fully replace the Bestmile apps.

The tech stack of the People Mobility web app:

That is a list of mature, well-established tools, which is exactly how I like it. Having everything in TypeScript, down to the generated API clients and types, is a real comfort. Faker and Mock Service Worker were new to me, and they make it much easier to write meaningful unit tests.

Beyond my usual priorities of good test coverage, consistent code and 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 | ╳          |               |               |
Summary of user permissions, generated by the unit tests
Unit test output, phrased as 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, migrating the whole UI to the new design system ran in the background of my other work. Design system migrations seem to be a recurring theme in my career.

The same screen in the old and the new design system
Migrating to the new design system, old on the left, new on the right. One of the main goals was a denser UI.
Navigation in other SCALAR apps compared with People Mobility before and after the migration
Navigation in other SCALAR apps, and in People Mobility before and after the migration

Bookings

One of my first features was the booking creation funnel. Compared with the Bestmile version, it added wheelchair bookings, configurable fare options (adult, child, dog, and so on) and the ability to reuse a past booking.

Passenger selection step
Passenger selection based on configurable fare options
Recent bookings offered as origin and destination
Reusing a traveler’s recent booking for the origin and destination
Route preview with walking legs
Route preview for a station-based service, including the walk to and from the stops

I made the whole funnel responsive, with the map on top on narrow screens instead of on the right, and the new design system gave us a dark theme for free.

Booking funnel on a narrow screen in the dark theme
Responsive layout and dark theme

A first version of the bookings list already existed. I added features such as ride rematching, for example to handle incidents, and ride flagging.

Bookings list with an expanded booking
Bookings list with booking details, and actions to flag, rematch or cancel a ride

Traveler management

Listing and editing travelers already existed, so I only added the ability to delete a traveler.

Traveler list and form
Traveler management, nothing fancy

Bestmile had a simple announcement system to show messages to users of the ride-hailing traveler apps. I ported it to the People Mobility app.

Traveler announcements list and form
Traveler announcements, a straightforward CRUD feature

Service configuration

Next came the service configuration screens. I enjoyed creating SVG diagrams to explain the dispatching parameters. I wanted them to adapt dynamically to the values the user enters; the team preferred static illustrations, but the code still supports it.

Dispatching parameters form with SVG diagrams
Hand-crafted SVG diagrams explaining the dispatching parameters
Animated SVG diagram reacting to input values
The SVG-generating code supports dynamic illustrations driven by the user’s input

Another fun but tricky piece was the three-way percentage split of the optimization weights: changing one value adjusts the other two, unless they are locked.

Three optimization weight sliders
Optimization weights
Animated sliders adjusting each other
An interaction that is harder to get right than it looks
The three-way percentage split, with its 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 });
    });
  });
});

The last notable part of service configuration was service hours, the time slots during which a transportation service is available. It was a perfect use case for React Hook Form’s useFieldArray.

const { fields, append, remove, insert, update } = useFieldArray({
  control,
  name: 'hours',
});
Service hours editor
A custom schedule for service hours, built on React Hook Form’s useFieldArray

Live operations

A basic version of the “Dispatching” screen already existed. I added many improvements, such as:

Live dispatching screen with map, vehicle list and ride details
The very information-dense live dispatching screen
Panels being resized
Resizable panels with minimum and maximum sizes; the current size is saved in the user’s preferences

Shift planning

I was less involved in the “Shift planning” screen, but touched almost every corner of its code while fixing bugs, adding tests and migrating it to the new design system.

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

I also prototyped drag and drop for vehicle shifts, with a “ghost” element that turns green or red to show where a shift can be dropped. It never shipped, but the experiment informed the upcoming Cargo Planning app.

Dragging a shift with a colored ghost element
Drag-and-drop prototype

Cargo Planning

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

Cargo planning screen with order list, timeline and form
Assigning customer orders (unassigned trips) to a driver, truck and trailer, by dragging them onto the timeline or through the form, with suggestions for chaining trips
Distance filter on the timeline
Filtering for trips that end near the start of the trip being planned, to encourage trip chaining and make better use of resources
Unassigned trips shown as a table in a wide panel
The unassigned trips list turns into a table when the left panel is wide enough

Working on this prototype was a lot of fun, with quick iterations on ideas. Mocking the API let us move fast without waiting on anyone.

It was also my first opportunity to use container queries.

Order details in one and two columns
Container queries show the order details in one column (left) or two (bottom), depending on the width of the container
<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 was my second job in a row focused entirely on frontend development. Some of the things that were new, at least to me:

Toward the end, I started experimenting with AI through Claude Code and Copilot. I found these tools useful for:

Overall, AI tools still felt early and experimental to me. I am curious to see how they evolve, and what remains once the hype settles.