fix: safe status-recommendation flow, MO-016 order independence, brand constant, message codes
- Add a single shared, pure vehicle-status evaluator (app/services/vehicle_status.py)
used identically by the data-quality scanner, a new non-mutating status-recommendation
preview endpoint, and a transactional apply endpoint with optimistic-concurrency token
revalidation -- eliminates the old opaque "calculate and apply" action and the unsafe
"maintenance + active booking -> auto rented" shortcut. Frontend
DataQualityIssueDetail.tsx now shows a review/decide/confirm panel with localized
why/evidence/consequence text in nl-BE/en-GB/fr-BE, with an exact "Change status to
<status>" confirm action per the brief.
- Fix MO-016 issue-order dependency: resolving the booking-overlap issue before vs.
after the status-conflict issue now converges on the same final vehicle status,
proven by test_mo_016_status_conflict_recommendation_is_order_independent.
- Make "Fleet Ops" a non-localizable brand constant (frontend/src/product.ts,
backend PRODUCT_NAME) via {{productName}} interpolation everywhere the brand name
appeared in locale prose; add a permanent test guarding against a translation file
ever defining the brand name or an "appName" key again.
- Convert dynamic backend prose to stable message codes + params: return status
reasons, audit field/actor-type labels, automation last_error, and search
section/vehicle/booking/issue results all now carry codes the frontend localizes,
with raw technical text demoted to a "Technical details" disclosure.
- docs/fleet-ops-correction/: gap audit, i18n inventory, and the vehicle-status
decision table documenting the evaluator's rules and safe-status principles.
148 backend tests + Ruff + mypy green; Alembic migration verified upgrade/downgrade;
frontend tsc/build and the i18n-coverage Playwright suite green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
18344bc8b7
commit
6deb95524d
@@ -40,6 +40,29 @@
|
||||
"was": "{{field}} : était {{value}}",
|
||||
"changed": "{{field}} : {{before}} → {{after}}"
|
||||
},
|
||||
"actorTypes": {
|
||||
"user": "Utilisateur",
|
||||
"system": "Système",
|
||||
"service": "Service"
|
||||
},
|
||||
"fields": {
|
||||
"status": "Statut",
|
||||
"operational_status": "Statut opérationnel",
|
||||
"registration_number": "Plaque d'immatriculation",
|
||||
"make": "Marque",
|
||||
"model": "Modèle",
|
||||
"location": "Emplacement",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"email": "E-mail",
|
||||
"phone": "Téléphone",
|
||||
"postal_code": "Code postal",
|
||||
"city": "Ville",
|
||||
"booking_end_odometer_km": "Kilométrage final de la réservation",
|
||||
"vehicle_odometer_km": "Kilométrage du véhicule",
|
||||
"survivor": "Profil conservé",
|
||||
"loser": "Profil fusionné"
|
||||
},
|
||||
"actions": {
|
||||
"demo_login": "Connecté",
|
||||
"demo_logout": "Déconnecté",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"orgLine": "Organisation de démo : {{orgName}} (fictive)",
|
||||
"headline1": "Chaque transfert.",
|
||||
"headline2": "Une vue claire.",
|
||||
"defaultDescription": "Fleet Ops rassemble les données de véhicules, de réservations et d'exploitation, soutient les processus de location, détecte les problèmes de qualité des données et automatise les suites contrôlées.",
|
||||
"defaultDescription": "{{productName}} rassemble les données de véhicules, de réservations et d'exploitation, soutient les processus de location, détecte les problèmes de qualité des données et automatise les suites contrôlées.",
|
||||
"footnote": "Démo synthétique · aucune donnée client ou véhicule réelle · réinitialisable à tout moment",
|
||||
"accessEyebrow": "Accès démo",
|
||||
"accessHeading": "Choisissez comment démarrer",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"appName": "Fleet Ops",
|
||||
"orgName": "Northstar Mobility",
|
||||
"brandTagline": "Centre de contrôle",
|
||||
"actions": {
|
||||
@@ -35,7 +34,7 @@
|
||||
"fr-BE": "Français"
|
||||
},
|
||||
"footer": {
|
||||
"productLine": "Fleet Ops Demo",
|
||||
"productLine": "{{productName}} Demo",
|
||||
"locale": "Europe/Bruxelles · Données de démonstration synthétiques"
|
||||
},
|
||||
"demo": {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"whatYouWillSee": "Un aperçu de ce qui est fonctionnellement implémenté dans cette démo, ce qui est synthétique, et quelles intégrations ne sont pas encore en direct.",
|
||||
"whyItMatters": "Une démo n'est convaincante que si les visiteurs peuvent vérifier eux-mêmes ce qui fonctionne réellement et ce qui reste à venir.",
|
||||
"startAction": "Lisez la page « À propos de cette démo ».",
|
||||
"expectedOutcome": "Vous pouvez expliquer vous-même ce que Fleet Ops est et n'est pas, sans explication verbale nécessaire."
|
||||
"expectedOutcome": "Vous pouvez expliquer vous-même ce que {{productName}} est et n'est pas, sans explication verbale nécessaire."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -146,16 +146,16 @@
|
||||
},
|
||||
"about": {
|
||||
"eyebrow": "À propos de cette démo",
|
||||
"title": "Ce que Fleet Ops est et n'est pas",
|
||||
"title": "Ce que {{productName}} est et n'est pas",
|
||||
"description": "{{orgName}} est une organisation de location fictive qui rend cette démo concrète — pas une véritable entreprise.",
|
||||
"loading": "Chargement des informations de démo…",
|
||||
"ctaTitle": "Vous préférez vous lancer directement ?",
|
||||
"ctaBody": "La démo guidée parcourt les huit étapes ci-dessus en pratique.",
|
||||
"ctaButton": "Démarrer la démo guidée",
|
||||
"problemTitle": "Le problème fictif",
|
||||
"problemBody": "{{orgName}} loue environ 50 campings-cars et fourgonnettes depuis un site principal. Les réservations, retours, fiches clients et l'entretien vivaient jusqu'ici dans des feuilles de calcul séparées et des échanges verbaux, si bien que les problèmes (clients en double, kilométrages incorrects, véhicules réservés en double) n'apparaissaient que tardivement. Fleet Ops montre comment un système unique et connecté signale ces problèmes tôt et permet de les résoudre de façon contrôlée.",
|
||||
"problemBody": "{{orgName}} loue environ 50 campings-cars et fourgonnettes depuis un site principal. Les réservations, retours, fiches clients et l'entretien vivaient jusqu'ici dans des feuilles de calcul séparées et des échanges verbaux, si bien que les problèmes (clients en double, kilométrages incorrects, véhicules réservés en double) n'apparaissaient que tardivement. {{productName}} montre comment un système unique et connecté signale ces problèmes tôt et permet de les résoudre de façon contrôlée.",
|
||||
"scopeTitle": "Pour qui et avec quelle portée",
|
||||
"scopeBody": "Cette démo s'adresse à quiconque veut voir comment Fleet Ops traite les problèmes opérationnels d'un petit loueur : Operations Managers et Rental Employees, et toute personne évaluant l'approche. La portée est délibérément limitée à une preuve de concept unique et cohérente — pas de comptabilité, pas de paiements, pas de réservations publiques, pas de CRM ou ERP complet.",
|
||||
"scopeBody": "Cette démo s'adresse à quiconque veut voir comment {{productName}} traite les problèmes opérationnels d'un petit loueur : Operations Managers et Rental Employees, et toute personne évaluant l'approche. La portée est délibérément limitée à une preuve de concept unique et cohérente — pas de comptabilité, pas de paiements, pas de réservations publiques, pas de CRM ou ERP complet.",
|
||||
"realTitle": "Ce qui fonctionne réellement",
|
||||
"realBody": "Tout ce qui suit est du code fonctionnel, pas seulement une maquette : accès et sessions basés sur les rôles, gestion des véhicules et réservations, traitement des retours avec validation côté serveur, cinq règles de qualité des données avec chacune sa propre étape de résolution, une piste d'audit complète, une livraison automatisée vers n8n avec nouvelles tentatives limitées, un déploiement basé sur Docker, et une suite de tests automatisés (backend et Playwright de bout en bout).",
|
||||
"syntheticTitle": "Ce qui est synthétique",
|
||||
|
||||
@@ -63,6 +63,14 @@
|
||||
"noQualityIssues": "Aucun problème de qualité enregistré.",
|
||||
"fuel": "Carburant {{percent}}%",
|
||||
"damage": "Dommage",
|
||||
"technicalWarning": "Avertissement technique"
|
||||
"technicalWarning": "Avertissement technique",
|
||||
"inspectionTypes": {
|
||||
"departure": "Inspection de départ",
|
||||
"return": "Inspection de retour"
|
||||
},
|
||||
"maintenanceCategories": {
|
||||
"periodic_service": "Entretien périodique",
|
||||
"repair": "Réparation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,13 @@
|
||||
"noAction": "—",
|
||||
"eventTypes": {
|
||||
"vehicle.returned.v1": "Retour de véhicule traité"
|
||||
},
|
||||
"errorCodes": {
|
||||
"connectionError": "Le service d'automatisation était temporairement injoignable. Le retour a été enregistré en toute sécurité et peut être soumis à nouveau.",
|
||||
"malformedPayload": "La tâche contenait des données incomplètes et n'a pas pu être livrée. Les données sous-jacentes restent enregistrées en toute sécurité.",
|
||||
"remoteReportedFailure": "Le service d'automatisation a refusé la livraison. La tâche peut être soumise à nouveau.",
|
||||
"staleLeaseRecovered": "Cette tâche a été récupérée après qu'une tentative de livraison précédente s'est arrêtée sans résultat.",
|
||||
"unknownError": "Une erreur inattendue s'est produite lors de la livraison de cette tâche."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"Quels contrôles sont requis avant le départ ?"
|
||||
],
|
||||
"emptyTitle": "Des preuves avant les réponses",
|
||||
"emptyDescription": "Posez des questions sur les retours, les dommages, les inspections ou une autre procédure indexée. Fleet Ops n'invente jamais de réponse en l'absence de preuves.",
|
||||
"emptyDescription": "Posez des questions sur les retours, les dommages, les inspections ou une autre procédure indexée. {{productName}} n'invente jamais de réponse en l'absence de preuves.",
|
||||
"retrievalFlow": {
|
||||
"question": "Question",
|
||||
"sources": "Sources",
|
||||
|
||||
@@ -27,12 +27,22 @@
|
||||
"resetConfirmYes": "Oui, réinitialiser",
|
||||
"resetCancel": "Annuler",
|
||||
"resetFailed": "Impossible de réinitialiser les données de démo.",
|
||||
"searchLabel": "Rechercher dans Fleet Ops",
|
||||
"searchLabel": "Rechercher dans {{productName}}",
|
||||
"searchPlaceholder": "Rechercher flotte, réservation ou section…",
|
||||
"searchShortcutHint": "Ctrl K",
|
||||
"searchSearching": "Recherche…",
|
||||
"searchUnavailable": "La recherche est momentanément indisponible.",
|
||||
"searchNoResults": "Aucun résultat pour « {{query}} ».",
|
||||
"searchVehicleSummary": "{{make}} {{model}} · {{location}}",
|
||||
"searchSections": {
|
||||
"overview": "Tableau de bord opérationnel",
|
||||
"fleet": "Registre de la flotte",
|
||||
"bookings": "Réservations de location",
|
||||
"quality": "Atelier qualité",
|
||||
"knowledge": "Assistant de procédures",
|
||||
"integrations": "Statut d'automatisation et d'intégration",
|
||||
"audit": "Historique d'audit"
|
||||
},
|
||||
"switchRole": "Changer de rôle",
|
||||
"switchRoleTitle": "Changer de rôle de démo",
|
||||
"languageSwitcherLabel": "Changer de langue"
|
||||
|
||||
@@ -180,13 +180,48 @@
|
||||
"heading": "Résoudre le conflit de statut",
|
||||
"description": "Une règle faisant autorité recommande un statut opérationnel corrigé pour ce véhicule.",
|
||||
"currentStatus": "Statut actuel",
|
||||
"calculateAndApply": "Calculer et appliquer le statut recommandé",
|
||||
"confirmTitle": "Confirmer le changement de statut",
|
||||
"confirmBody": "Appliquer le statut recommandé faisant autorité pour ce véhicule ?",
|
||||
"reviewRecommendation": "Voir la recommandation",
|
||||
"loadingRecommendation": "Chargement de la recommandation…",
|
||||
"recommendationFailed": "Impossible de charger une recommandation.",
|
||||
"recommendedStatus": "Statut recommandé",
|
||||
"whyHeading": "Pourquoi",
|
||||
"evidenceHeading": "Faits",
|
||||
"consequenceHeading": "Conséquence",
|
||||
"consequence": {
|
||||
"statusWillChange": "Le statut du véhicule sera changé en {{status}}.",
|
||||
"issueWillBeRechecked": "Ce problème de qualité sera à nouveau contrôlé.",
|
||||
"changeWillBeAudited": "Le changement sera enregistré dans la piste d'audit.",
|
||||
"bookingsNotDeleted": "Les réservations elles-mêmes ne sont pas supprimées."
|
||||
},
|
||||
"changeStatusTo": "Changer le statut vers {{status}}",
|
||||
"applying": "Application en cours…",
|
||||
"confirmYes": "Oui, appliquer",
|
||||
"applied": "Appliqué {{status}} — {{reason}}",
|
||||
"applyFailed": "Impossible d'appliquer un statut recommandé."
|
||||
"applyFailed": "Impossible d'appliquer un statut recommandé.",
|
||||
"staleRecommendation": "La situation a changé depuis l'affichage de cette recommandation. Consultez-la à nouveau avant de l'appliquer.",
|
||||
"reviewAgain": "Revoir la recommandation",
|
||||
"manualReview": {
|
||||
"heading": "Évaluation manuelle requise",
|
||||
"body": "Les faits concernant ce véhicule se contredisent. Un Operations Manager doit évaluer la situation en personne plutôt que d'appliquer un changement de statut automatique.",
|
||||
"hint": "Utilisez « Reporter » ou « Rejeter » ci-dessous, ou examinez manuellement le véhicule et les réservations concernées."
|
||||
},
|
||||
"noConflict": {
|
||||
"heading": "Aucun changement nécessaire",
|
||||
"body": "Le statut actuel de ce véhicule correspond déjà aux faits."
|
||||
},
|
||||
"evidence": {
|
||||
"activeBookings": "Réservation(s) active(s) : {{refs}}",
|
||||
"overlappingBookings": "Réservations qui se chevauchent : {{pairs}}",
|
||||
"serviceThresholdReached": "Le kilométrage ({{odometer}} km) a atteint le seuil d'entretien ({{threshold}} km).",
|
||||
"openOverlapIssue": "Problème de chevauchement de réservation ouvert : {{ref}}"
|
||||
},
|
||||
"reasonCodes": {
|
||||
"vehicle.active_rental": "Ce véhicule a une location active mais n'est pas enregistré comme loué. Le statut est corrigé pour refléter la situation réelle.",
|
||||
"vehicle.service_threshold_reached": "Ce véhicule a atteint son seuil d'entretien. Il est placé en entretien afin qu'il ne soit pas utilisé avant que la révision ait eu lieu.",
|
||||
"vehicle.booking_conflict": "Ce véhicule a deux réservations qui se chevauchent. Le bloquer évite qu'il soit à nouveau considéré comme disponible avant que le conflit de réservation ne soit résolu.",
|
||||
"vehicle.rental_ended": "La location de ce véhicule est terminée et il n'y a plus de réservation active. Il est remis en disponible.",
|
||||
"vehicle.manual_review_required": "Les faits concernant ce véhicule se contredisent (par exemple, une réservation active alors que le véhicule est en entretien, ou un seuil d'entretien atteint en même temps qu'une location active). Cela nécessite une évaluation humaine plutôt qu'un changement de statut automatique.",
|
||||
"vehicle.no_conflict": "Le statut actuel de ce véhicule correspond déjà aux faits. Aucun changement n'est nécessaire."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,13 @@
|
||||
"odometerRegressionWarning": "Le kilométrage saisi ({{submitted}} km) est inférieur au relevé de référence ({{canonical}} km). Le kilométrage de référence ne changera pas et un problème de qualité des données sera ouvert.",
|
||||
"nextBookingRisk": "La prochaine réservation {{ref}} débute {{when}} — peut être affectée par ce retour.",
|
||||
"nextBookingLowRisk": "La prochaine réservation {{ref}} débute {{when}} — risque faible.",
|
||||
"reasonCodes": {
|
||||
"returnBlockedDamageAndTechnical": "Des dégâts et une alerte technique ont tous deux été signalés lors de ce retour.",
|
||||
"returnBlockedDamage": "Des dégâts ont été signalés lors de ce retour.",
|
||||
"returnBlockedTechnicalWarning": "Une alerte technique a été signalée lors de ce retour.",
|
||||
"returnServiceThresholdReached": "Le kilométrage a atteint le seuil d'entretien de {{threshold_km}} km.",
|
||||
"returnRoutedToCleaning": "Aucun dégât, alerte technique ou seuil d'entretien atteint ; le véhicule part en nettoyage."
|
||||
},
|
||||
"commitList": {
|
||||
"inspection": "Créer une inspection de retour",
|
||||
"updateAtomic": "Mettre à jour la réservation et le véhicule de façon atomique",
|
||||
@@ -66,8 +73,9 @@
|
||||
"continueDemo": "Poursuivre la démo"
|
||||
},
|
||||
"scenario": {
|
||||
"ariaLabel": "Scénario de démo",
|
||||
"title": "Scénario de démo : anomalie de kilométrage",
|
||||
"body": "Ce véhicule affiche actuellement {{odometer}} km. Le formulaire ci-dessous est pré-rempli avec un relevé de retour inférieur — signe d'une erreur de saisie ou d'un véhicule confondu. Confirmez le retour pour voir comment Fleet Ops détecte et traite cela.",
|
||||
"body": "Ce véhicule affiche actuellement {{odometer}} km. Le formulaire ci-dessous est pré-rempli avec un relevé de retour inférieur — signe d'une erreur de saisie ou d'un véhicule confondu. Confirmez le retour pour voir comment {{productName}} détecte et traite cela.",
|
||||
"preparing": "Préparation du scénario…"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user