04 — Fleet Services Group
Un moteur d’ordonnancement qui explique ses refus
Un outil où chacun dépose sa demande avec sa date limite, et qui construit tout seul le planning de l'équipe — horaires, pauses, congés et jours fériés compris. Quand la date n'est pas tenable, il dit pourquoi et à qui s'adresser.
Ciblage, intégration, rectification, demandes de la direction : le pôle data traite des demandes de nature très différente, avec des charges très variables et des échéances qui tombent toutes le vendredi. Sans outil, la question « est-ce que ça rentre avant vendredi ? » ne recevait qu'une réponse au doigt mouillé.
Le problème
Les demandes arrivaient par mail, par messagerie, parfois de vive voix. Aucune file unique, donc aucune priorisation. Aucune visibilité sur la charge réelle, donc des engagements pris à l'aveugle. Et un planning hebdomadaire reconstruit à la main, faux au premier imprévu.
Le vrai enjeu n'était pas de faire un outil de tickets de plus : c'était de répondre automatiquement à la question de la faisabilité. Un demandeur doit savoir, au moment où il dépose sa demande, si l'échéance qu'il propose est tenable — et si elle ne l'est pas, ce qu'il faudrait décaler.
Ce que le moteur doit respecter simultanément
- Les horaires d'ouverture et la pause déjeuner : une tâche de six heures déposée à 16 h ne se termine pas à 22 h.
- Les week-ends et les jours fériés — y compris les fériés mobiles, qui dépendent de la date de Pâques.
- Les absences : congés, formations, jours non travaillés. La capacité change chaque semaine.
- Une réserve d'urgence quotidienne : le pôle se garde une heure par jour, dans laquelle seules les demandes marquées urgentes ont le droit de mordre.
- Le parallélisme différencié : certaines tâches peuvent avancer de front, d'autres monopolisent une personne du début à la fin. Un moteur qui traite tout pareil produit un planning que personne n'applique.
- Les échéances imposées par le client final, qui ne se négocient pas.
Décisions structurantes
01Une algèbre d’intervalles plutôt qu’une boucle sur des créneaux
Le socle est un ensemble de neuf fonctions pures, sans état ni dépendance : soustraction d'ensembles d'intervalles par balayage, réservation depuis la fin, prélèvement d'un volume d'heures avec découpe à la milliseconde. Tout le raisonnement se compose alors en quatre appels chaînés — fenêtres du jour, moins la réserve, bornées à la période, moins les créneaux occupés et les absences. Pas une seule boucle sur des créneaux discrets. C'est ce qui distingue un développeur qui modélise d'un développeur qui écrit des boucles.
02Les jours fériés sont calculés, pas maintenus
Plutôt qu'une dépendance externe ou une table à mettre à jour chaque année, la date de Pâques est calculée par l'algorithme de Gauss, et les trois fériés mobiles en découlent. Fusionnés avec les huit fériés fixes et mémoïsés par année. Ça fonctionne pour n'importe quelle année, à perpétuité, sans réseau et sans maintenance.
03Le changement d’heure a été traité, pas contourné
C'est le piège numéro un d'un planning : itérer sur les jours en ajoutant 24 heures, et se retrouver décalé après le passage à l'heure d'hiver. Ici, l'énumération convertit d'abord dans l'espace local, itère en arithmétique calendaire, puis reconvertit chaque jour en instant absolu. Les jours restent ancrés à minuit heure locale à travers les deux bascules annuelles, quel que soit le fuseau du serveur. Les horaires sont stockés en minutes depuis minuit, et le fuseau est une donnée de configuration, pas une constante.
04Le refus n’est jamais un mur
Quand aucune place ne rentre avant l'échéance, le moteur ne s'arrête pas. Il relance une recherche sur 180 jours pour déterminer la première date réellement atteignable, chiffre l'écart exact entre heures disponibles et heures nécessaires, puis l'orchestrateur va chercher en base quels demandeurs occupent le créneau contesté, les agrège par personne et les trie par nombre de tâches bloquantes. L'écran affiche alors : « pas avant le 12 juin ; si vous voulez passer devant, voici les trois personnes à voir, dans cet ordre ».
05Le résultat est une union discriminée, pas un objet à champs optionnels
Le type de retour du moteur porte un discriminant succès/échec. TypeScript refuse de laisser lire les créneaux sans avoir vérifié le succès. Le cas d'échec ne peut donc pas être oublié — il est structurellement obligatoire de le traiter, ce qu'un objet à champs optionnels n'aurait jamais garanti.
06La simulation rejoue le même moteur, sans rien écrire
Un point d'entrée dédié appelle exactement la même fonction de placement sans toucher à la base. Côté interface, un effet avec temporisation de 350 millisecondes déclenche la simulation pendant que l'utilisateur ajuste sa durée ou son échéance — il voit son créneau se recomposer jour par jour avant même de valider. Le paramètre qui exclut les créneaux du ticket en cours de replanification est le genre de détail qu'on ne découvre qu'en production : sans lui, un ticket se voit lui-même comme occupé et ne peut jamais rester à sa place.
07Authentification d’entreprise, sans raccourci
Flux d'autorisation avec code et vérificateur cryptographique, jeton anti-rejeu généré et comparé côté serveur, provisionnement automatique au premier accès avec le rôle le moins privilégié, et l'utilisateur rechargé depuis la base à chaque requête — une désactivation prend effet immédiatement, pas au prochain login. Le contrôle d'accès est monté une seule fois sur le préfixe de l'API : il est impossible d'oublier de protéger une route, le défaut est fermé.
Côté interface
Sept écrans, dont un tableau de bord qui distingue les demandes en retard, un calendrier qui n'affiche que les heures ouvrées et grise les absences en fond de grille, et un tableau d'avancement.
La gestion d'état sépare strictement l'état serveur — confié à une bibliothèque de cache de requêtes — et l'identité, seule chose réellement globale, dans un contexte dédié. Aucun gestionnaire d'état global, aucune donnée serveur dans un contexte React. L'invalidation de cache suit fidèlement les effets de bord du serveur : créer une absence invalide non seulement les absences, mais aussi le planning et les tickets, parce que le serveur déclenche une replanification. C'est du travail soigné, et c'est ce qui évite les écrans qui affichent des données périmées.
Ce que je ferais évoluerDéplier
- Le moteur n'est pas testé, alors qu'il est entièrement conçu pour l'être : fonctions pures, aucune I/O, une graine de testabilité déjà présente. Une suite de deux cents lignes couvrirait l'essentiel. C'est la première chose que j'écrirais.
- Les types ne sont pas partagés entre le serveur et l'interface — ils sont redéclarés à la main, et le type de résultat y perd son discriminant. C'est la principale dette d'architecture.
- Un contrôle d'autorisation manque sur la modification d'un ticket : l'authentification est exigée, mais pas la propriété. À corriger avant tout usage élargi.
- Les sessions vivent en mémoire du processus : suffisant pour l'usage interne actuel, pas pour un vrai déploiement.
Stack
Backend
- Node.js
- TypeScript strict
- Express
- Prisma
- PostgreSQL 16
- Zod
- Pino
Frontend
- React
- TypeScript
- Vite
- TanStack Query
- Tailwind CSS
- Radix
Interactions
- FullCalendar
- date-fns-tz
Infrastructure
- Azure AD / Entra ID (MSAL)
- Docker
- Migrations versionnées
- Journal d’audit