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:
- help implement a new design system to be used by all teams inside ZF SCALAR
- finish the migration of the Bestmile web apps to the ZF SCALAR suite
- prototype a web app to plan resources for Cargo transportation
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.

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:
- code versioning: GitLab
- dependency management: pnpm with automated updates via Renovate
- language: TypeScript
- framework: React 18
- build tools: Vite and Nx
- API interfacing: OpenAPI and Tanstack Query
- app routing: React Router
- maps: deck.gl, luma.gl, react-map-gl, Turf JS
- authentication: Auth0
- feature flags: Unleash
- unit tests: Vitest, Testing Library, Faker JS, Mock Service Worker
- internationalization: i18next
- translations: Crowdin
- documentation: Storybook
- code linting: ESLint
- code formatting: Prettier
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:
- implement a system to save user preferences like opened/closed panels, panel sizes, filters states, visible map layers, visible table columns, … in localstorage which improved the user experience so much
- overhaul the language management. For example, prior to my changes, all language files were loaded all the time
- add access checks to the whole app
| | Dispatcher | Booking Agent | Cargo Planner |
| ----------------------------- | ---------- | ------------- | ------------- |
| PeopleMobility app | ╳ | ╳ | |
| Bookings module | ╳ | ╳ | |
| Dispatching module | ╳ | ╳ | |
| Scheduling module | ╳ | | |
| Services module | ╳ | | |
| Travelers module | ╳ | ╳ | |
| Planning module | | | ╳ |
| TravelerAnnouncements feature | ╳ | | |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" tabBookings > 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 errorBookingCreation > 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 funnelFor 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.


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.



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.

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.

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

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.

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.


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.


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',
});

Live operations
A basic version of the “Dispatching” screen already existed, but I worked on many improvements like:
- show the assigned/actual driver, status, driver shift progress
- details of the next mission when expanding the vehicle details
- ability to flag or rematch a ride
- allow to pause/resume a driver shift
- resizable panels so users can optimize their usage according to the screen real-estate they have


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.


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.

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:
- order details in the order list. Turn the list into a table when the panel gets wide enough
- drag’n’drop from the order list to the timeline
- drag’n’drop within the timeline
- use fake OpenAPI specs to generate a fake API client with mocked responses. Then later swap out the fake API with the real one, and stay in sync with on-going API changes
- trip preview showing the schedule suggested by the system, before the user either confirms or backs out
- detect order pickup or delivery time window violations, and show warnings accordingly
- filter items on the timeline by distance
- filter panel
- add support for translations
- ensure a proper module and library separation in the monorepo that also contains the People Mobility app
- improve a11y



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!

<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:
- the container queries I mentioned above
- flow-relative logical values, e.g. use margin-inline-start instead of margin-left
- the CSS resize property which is handy for documenting components
- ResizeObserver, which I still find confusing to use, and that allows to execute code when something is resized
- repeating linear gradient for stripes in backgrounds
- extensively using the HTML Drag’n’drop API
- relying to the browser’s built-in Internationalization API instead of using an external library for date, time, distance, ISO duration, speed and temperature formatting
- having a monorepo containing 2 apps, 8 modules, and numerous libraries held together via nx and strict module boundaries
- dependency management with pnpm instead of npm
I only recently started to dabble with AI via Claude Code and Copilot. I can see how those tools can be useful for:
- explaining unfamiliar codebases
- generating unit tests
- simplifying or refactoring complex code. I haven’t had success yet when trying to refactor substantial sections of an app.
- help understand error messages or stack traces
- code something in a domain you have very little experience
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.