Commit Graph

141 Commits

Author SHA1 Message Date
louispaulb
4bed6bb9fd planif : extraction #4/2 — dialogue Synchroniser les techniciens → components/planif/TechSyncDialog.vue
2e extraction de la décomposition (couplage nul : ne lit AUCUN état partagé).
- components/planif/TechSyncDialog.vue (NOUVEAU) : reactive techSync + techSyncSelCount +
  applyTechSync déplacés ; rapport self-fetch à l'ouverture (watch modelValue) ; v-model
  pour l'ouverture ; émet `applied` après création → le parent appelle loadBase
- PlanificationPage : dialogue + reactive + 2 fonctions retirés ; menu Outils → `showTechSync = true` ;
  `<TechSyncDialog v-model="showTechSync" @applied="loadBase" />`
- Vérifié en aperçu : ouvre via le menu, rapport chargé (staff 53 · fiches 56 · tech 41), 0 erreur

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:02:10 -04:00
louispaulb
659cfae12c planif : extraction #4/1 — dialogue Congés & disponibilités → components/planif/LeaveDialog.vue
Début de la décomposition de PlanificationPage (6725 l.). 1re extraction = le panneau
le plus AUTO-CONTENU (survey de couplage) : congés/disponibilités.

- components/planif/LeaveDialog.vue (NOUVEAU, 78 l.) : état privé (leaveRows/leaveFilter/
  newLeave) + fonctions (loadLeave/approveLeave/createLeave) déplacés tels quels ; v-model
  pour l'ouverture ; techs + techOptions en props LECTURE SEULE ; charge à l'ouverture (watch) ;
  CSS .demand-tbl recopié (le scoped ne traverse pas vers l'enfant)
- PlanificationPage : dialogue + état + 4 fonctions retirés (−39 l.) ; menu Outils → `showLeave = true`
- AUCUNE mutation d'état partagé (pas de loadWeek) → ne peut pas désynchroniser la grille
- Vérifié en aperçu : ouvre via le menu, table + formulaire rendus, chargement OK, CSS 11px appliqué

Prochaines extractions sûres (par couplage croissant) : techSync, types de shift, puis
absence/demande (loadWeek), enfin les couplés (team/tags/garde/publish).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 21:45:22 -04:00
louispaulb
987ae98bfc planif : éditeurs de compétences unifiés sur TagEditor (fin de SkillSelect)
Régression corrigée : les 4 sélecteurs de compétences de Planification utilisaient
SkillSelect (chips plates, CSV, sans couleurs ni catalogue) au lieu du composant
riche TagEditor déjà employé pour les compétences d'un technicien.

- jobDetail + feuille du pool (« Compétences requises ») : SkillSelect → TagEditor
  (chips COLORÉES, autosuggest depuis tagCatalog, création à la volée + palette 12 couleurs)
- table d'équipe + table de demande de personnel : idem → TagEditor (compact)
- SkillSelect.vue SUPPRIMÉ (0 référence) ; import retiré
- normSkillList() : normalisation unique (TagEditor émet un tableau de libellés/{tag} ;
  rétro-compat CSV) → réutilisée par jobDetail/pool/équipe/demande
- demande : d.skills passe de CSV à tableau (migration au chargement + CSV rétabli
  à la génération des besoins)

RÉUTILISER les composants partagés (TagEditor pour toute saisie de compétences/tags),
ne pas reconstruire d'équivalent inférieur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 21:06:15 -04:00
louispaulb
7ca3ceef66 ops : priorités 3 niveaux (source unique) + compétences multi + création unifiée + planif
- config/dispatch-priority.js (NOUVEAU) : DISPATCH_PRIORITIES = SOURCE UNIQUE des 3 niveaux
  (Urgent/Moyenne/Basse, couleurs 🔴🟠) — élimine 5 copies locales (UnifiedCreateModal,
  TaskNode, CreateOfferModal, TechJobDetailPage, PlanificationPage POOL_PRIOS) et corrige
  la dérive « Moyenne » bleue de CreateOfferModal
- PlanificationPage/TechJobDetailPage : éditeur de compétences SkillSelect → /roster/job-skills ;
  FIX : SkillSelect émet une CSV (pas un tableau) → les handlers n'effaçaient plus les compétences ;
  panneau job (statut/reporter/réserver un tech) ; sommaire avant publication + auto-save + journal + annuler
- UnifiedCreateModal/ProjectWizard/QuoteWizard/TaskNode : formulaire de création unifié,
  divulgation progressive, dédup client (courriel/téléphone) + message d'erreur convivial ;
  suppression de NewTicketDialog (fusionné)
- IssueDetail : fil de ticket (nom d'expéditeur) ; app terrain lecture/commentaire
- useConversationDisplay/useHelpers/wizard-constants : priorités alignées sur 3 niveaux

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:46:57 -04:00
louispaulb
0ba789a5c0 planif Notifier : toggles SMS/courriel PAR TECH (SMS préféré) + édition inline du contact
- Chaque tech a ses propres toggles SMS/Courriel (cochés selon l'info dispo, SMS préféré) → on peut exclure
  quelqu'un (ex. envoi test). Envoi scindé : 1 appel SMS (techs cochés+tél) + 1 appel courriel (cochés+email).
  Bouton « Envoyer (N) » = nb réel de destinataires.
- Édition INLINE dans le dialogue : tech sans téléphone/courriel → champ + « Enregistrer » sans quitter
  (roster.setTechContact → POST /roster/technician/contact → update Dispatch Technician + invalide le cache).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 14:04:11 -04:00
louispaulb
d6db1a5fa4 planif: contrôle Statut/Reporter aussi sur le panneau job (3 vues couvertes)
jobDetail porte désormais .status ; TicketStatusControl (Dispatch Job) dans l'en-tête du volet job
(Reporter = replanifie scheduled_date +N · Fermer=Completed · Rouvrir=open). Le contrôle est maintenant
présent sur les 3 vues (page ticket · fiche client · Planification). Reste (séquencé) : convergence layout/sections #27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:57:41 -04:00
louispaulb
8a655f9933 planif: assistant renfort — nom robuste (jamais « undefined »)
jdAddAssistant : dérive le nom du tech chargé OU du libellé de l'option choisie OU l'id (jamais undefined),
coerce tech_id en String → la réservation (bloc hachuré) se crée correctement. Puce : repli techNameById/id/—.
(DB vérifiée : 0 ligne assistant corrompue ; le « undefined » était transitoire à l'ajout.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:36:43 -04:00
louispaulb
969d5af9f9 planif: horaires (lot) — recherche/skill/tout-aucun ; grille 2 sem. par défaut (sélecteur retiré)
- WeeklyScheduleEditor (lot) : recherche par nom + chips « par compétence » (sélectionne les techs qui l'ont)
  + boutons Tout / Aucun. Passe t.skills depuis openSchedGenBulk. Avant : liste de chips sans filtre.
- Grille : 2 semaines (days=14) par défaut, sélecteur « Semaine/2 sem. » retiré (évitait les changements d'étendue accidentels).
- (Rappel : onScheduleApply recharge déjà la grille → les quarts générés en lot apparaissent après application.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 12:56:17 -04:00
louispaulb
712f113e29 planif barre d'outils : sélecteur de vue à gauche, Publier unifié, picker single-job filtré
- Vues (Semaine/Jour/Tournées) déplacées À GAUCHE, mises en évidence (view-switch) ; retrait du titre
  « Planification » redondant (déjà au-dessus) → moins de déplacement d'éléments.
- « Publier » unifié en split-button : action principale + sous-options (☑ SMS · Publier au legacy).
- Picker « déplacer UNE job vers un autre tech » : filtré aux techs sélectionnés+compétents de la simulation
  (+ « Afficher tous »). Avant : le repli montrait tout le monde (techsForJob n'expose pas .skills → covers échouait).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 12:41:37 -04:00
louispaulb
9f2fb0eaf9 planif: liens Street View + Google Maps/satellite dans le volet détail job (géoloc)
Sous l'adresse, quand le job a des coordonnées : lien « Street View » (vue au sol, évaluer la difficulté)
+ « Carte / satellite » (Google Maps). Réutilise le format pano existant (openStreetView).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 12:21:15 -04:00
louispaulb
4ae2907172 solveur : le RANG (compétence principale) domine la polyvalence — répare la mauvaise répartition
Bug (Louis) : Stephane (réparation rang 1, installation secondaire) recevait 3 installations, William
(installation rang 1/principale) une seule. Cause : versatility_weight=6 > rank_weight=2 → un tech moins
polyvalent mais NON spécialiste rafflait les jobs hors de sa spécialité. Fix : rank_weight=20 (domine),
versatility_weight=3 (léger départage entre techs de même rang → réserve les experts). Testé : William obtient
les installations. eff/AM-PM/distance inchangés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 12:10:37 -04:00
louispaulb
88a5e84f90 planif Suggérer : UNE seule méthode (VRP) + retrait des réglages de coûts
Retire le sélecteur de stratégies (5 modes) et les champs Coûts (Spécialité/Heures sup/km-h/Calcul s)
mal compris. Méthode unique = solveur VRP, forcée. Description claire des critères pris en compte.
(Backend : brancher l'efficacité par skill + réserve-experts dans le solveur = étape suivante.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:47:17 -04:00
louispaulb
61a2f1000f planif: efficacité GLOBALE en % intuitif (haut = plus rapide), cohérent avec le per-skill
Le champ « Cadence » global était un FACTEUR brut (step 0.05, bas=rapide) → taper 1000 = facteur 1000
= catastrophiquement lent. Passé en % via effPctOf/factorFromPct (100/x) : 1000 % = facteur 0.1 = 10× plus
rapide, 90 % = plus lent. Renommé « Efficacité % » + tooltip. Le per-skill était déjà en % (inchangé).
NB : l'efficacité n'influe que sur les stratégies heuristiques (Intelligent/Meilleurs/Équilibré/Juste-ce-qu'il-faut),
PAS sur « Optimiser » (VRP OR-Tools) qui optimise routes+compétences+capacité sans facteur d'efficacité.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:19:29 -04:00
louispaulb
3a0b65942c planif Suggérer : ordre AM→PM des arrêts + picker « déplacer vers » filtré
- nnOrder respecte la fenêtre horaire : bloc AM (8-12) → sans-fenêtre → PM (12-16), plus-proche-voisin
  DANS chaque bloc. Avant : NN pur → une job PM pouvait finir 1re, une « premier en AM » 2e (bug signalé).
  Utilise le toggle AM/PM explicite OU detectAmpm (texte : « **premier en AM** », « PM Fin de journée »).
- Picker « déplacer vers un autre tech » : n'affiche que les techs SÉLECTIONNÉS + compétents par défaut,
  + « Afficher tous les techniciens… » pour lever le filtre (repli sur tous si aucun sélectionné-compétent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:14:07 -04:00
louispaulb
746a8de6df planif: fix figement carte des tournées suggérées (boucle de rendu) + tracés routiers réels
Cause : :routes="suggestRoutes.map(...)" inline créait un NOUVEAU tableau à chaque rendu → watch deep
de RouteMap → draw → emit('metrics') → onRouteMetrics mutait realRoutes → re-rendu → nouveau tableau →
BOUCLE INFINIE (onglet figé ; le remplacement asynchrone OSRM ne finissait jamais → lignes à vol d'oiseau).
Fix : computed CACHÉ suggestMapRoutes (réf stable) + onRouteMetrics idempotent. La géométrie routière OSRM
(vérifiée côté serveur : 200, ~405 pts) s'affiche maintenant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:57:39 -04:00
louispaulb
8586b9310a planif: Suggérer ne fige plus — timeout solveur + dialogue toujours fermable
- roster API : AbortController (timeout 45s, 90s pour /optimize-plan) → un solveur/hub lent ou
  injoignable lève au lieu de laisser l'await pendre à l'infini (suggestDlg.building coincé à true).
  Au-delà → repli heuristique local, dialogue utilisable.
- q-dialog Suggérer : @hide remet building=false (jamais coincé-ouvert au ré-open).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 09:56:12 -04:00
louispaulb
042c5545f7 ops: MapLibre/OSM basemap (drop Mapbox) + satellite toggle, unified Créer flow, wizard pricing, infra-first reverse client
- Maps: MapLibre GL + self-hosted Protomaps tiles + ESRI satellite toggle
- FAB 'Créer' chooser (incl. Soumission wizard) via useCreateSignal
- Quote wizard: new default tiers (80/500/1500 @ 39.95/49.95/49.95), competitor
  price-match, mini-map pin w/ green check, carry qualified address
- api/address.reverse() + reverseGeocodeOSM tries RQA-backed hub, Nominatim fallback

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:52:14 -04:00
louispaulb
7da4b1289c feat(ops+hub): notify techs of their tournée (SMS/email + token link + flags)
À la publication (vue Tournées), bouton « Notifier » → aperçu par tech
PUIS envoi. Chaque tech reçoit :
- UN lien token personnel (techFieldLink = app terrain, ses billets du
  jour à répondre),
- la liste de ses interventions avec ADRESSE + flags cruciaux (AM/PM ·
  URGENT · Appeler avant) détectés du titre/notes/priorité.

Hub (lib/roster.js) : composeDispatchNotifs(dates, techIds) + route
POST /roster/notify-dispatch (preview=compose sans envoi | envoi SMS via
twilio + Email via lib/email.js, best-effort par tech, retourne compte +
erreurs). jobFlags() détecte AM/PM/urgent/appeler-avant.
Client : api roster.notifyDispatch ; dialogue APERÇU (toggles SMS/Courriel
+ liste par tech + corps du message, alerte si pas de tel/courriel) →
« Envoyer ». Aperçu d'abord = pas d'envoi accidentel.

Vérifié : hub node --check OK ; client rend le bouton + dialogue + toggles,
0 erreur. ⚠️ Le PREVIEW/envoi nécessite le déploiement HUB (nouvelle
route) — GATÉ + sortant. Faire un test 1 tech avant usage réel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 09:24:21 -04:00
louispaulb
add3c08cee feat(ops): Suggérer — re-optimize existing (dispatched) jobs as a simulation
Suggérer peut servir de SIMULATION sur des jobs DÉJÀ dispatchés : nouveau
toggle « Ré-optimiser aussi les jobs déjà assignés » dans la config. Quand
activé, le solveur VRP reçoit AUSSI les noms des jobs assignés de la
fenêtre (assignedNamesForWindow, depuis occByTechDay) en plus du pool →
repropose une répartition de TOUT (existant + nouveaux jobs ajoutés).
Force le solveur (l'heuristique locale n'a que le pool en mémoire).
Aperçu seulement — rien n'est écrit avant « Appliquer » (qui réassigne).

Vérifié : toggle rendu dans la config Suggérer, 0 nouvelle erreur. Le
résultat de ré-optimisation (solveur roster-solver) est à confirmer en
prod (comme les autres runs solveur).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:47:31 -04:00
louispaulb
92c320d08f fix(ops): job detail — show all fields (raw pool job), full title, assign tech
Cause du « on ne voit qu'une partie du titre, aucun autre détail » :
openJobDetail lisait la forme BLOC (b.customer/skill/detail/legacy_id)
mais reçoit un job BRUT du pool quand ouvert depuis une goutte/carrousel
(customer_name/required_skill/legacy_detail/legacy_ticket_id) → tous les
champs vides sauf le titre. Corrigé : mappe les DEUX formes (client,
compétence, adresse, description, n° ticket, date) + charge le fil du
billet via le bon id → détails + commentaires s'affichent.

- Titre : plus tronqué (ellipsis → retour à la ligne, titre complet).
- Assignation DEPUIS le volet : sélecteur « Assigner à… / Réassigner… »
  (jdAssignTech) ; si d'autres tâches à la MÊME adresse/client/jour →
  propose de les assigner au même tech (réutilise assignNames +
  siblingJobs, comme la grille).

Vérifié live (goutte carrousel, ticket #253649) : titre complet non
tronqué, méta = compétence tv + adresse 44 Carre Morrison + dept + date,
section commentaires + sélecteur d'assignation présents ; 0 erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:20:05 -04:00
louispaulb
98184d0a61 feat(ops): job detail — map collapsed, duration adjust; popup width caps
Retours terrain sur les modales de détail job (carte Tournées) :
- Volet détail 1 job : la CARTE (jd-map + tracé GPS) n'est pas requise pour
  lire le ticket → repliée par défaut derrière « Voir la carte / tracé
  GPS » (init à la demande, pas au chargement). Détails simples d'abord +
  champ « Durée estimée » ajustable (comme « Suggérer ») qui persiste
  duration_h (+ override de session). Titre déjà complet.
- Popups « liste des jobs à cette adresse » débordaient hors écran sans
  scroll : maxWidth 260 + overflow-wrap + max-height 44vh scroll (carte
  jour du panneau À assigner ET popup groupée RouteMap).

Vérifié live : volet 1 job → carte masquée par défaut, bouton déplie +
init canvas OK, champ durée = 2.25h ajustable ; 0 nouvelle erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:06:02 -04:00
louispaulb
3979f79283 feat(ops): auto-detect AM/PM from ticket title + AM/PM in RDV proposal
1. Détection auto AM/PM : les tickets legacy « Note AM » / « Note PM »
   (importés tels quels) sont détectés (detectAmpm : titre + note, tokens
   AM/PM / avant-midi / après-midi, null si ambigu) et la contrainte
   horaire est PRÉ-APPLIQUÉE dans « Suggérer » (suggestDlg.jobTime seedé à
   l'ouverture, marqueur  auto_awesome sur la puce, modifiable). Le
   solveur respecte alors la fenêtre 8-12 / 12-16.
2. Fenêtre RDV « 3 dispos du client » : chaque créneau proposé a un
   sélecteur AM/PM (avant-midi/après-midi) qui règle l'heure représentative
   (AM=08:00, PM=13:00), heure exacte toujours ajustable. Aligné sur AM/PM
   du dispatch.

Vérifié live : Suggérer affiche auto_awesome + puce AM pré-cochée (jobs
« Note AM ») ; RDV → clic PM règle 13:00, AM règle 08:00 ; 0 nouvelle
erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:54:46 -04:00
louispaulb
aa52a2c98c feat(ops): map unassigned jobs grouped by address + overlap fan
#3 Regroupement par adresse : routeUnassignedPins groupe les jobs non
assignés d'une même adresse en UNE pastille portant les icônes des types
de jobs (distinctes, max 3) + le TEMPS TOTAL + un compteur ; couleur =
priorité la plus haute. Clic (tap mobile) sur une adresse à >1 job →
popup listant chaque job (sujet + durée), chaque ligne ouvre le détail →
plus besoin de cliquer chaque ticket, info visible d'un coup d'œil.

#4 Chevauchement : les gouttes non assignées rejoignent recluster()/fan()
(déjà éprouvé sur les arrêts assignés) → les pastilles proches s'éclatent
en éventail pour rester distinguables/atteignables. (Le regroupement par
adresse réduit déjà le fouillis ; le fan gère les adresses proches ≠.)

Vérifié par injection du markup (impossible de rendre la carte en dev,
jeu sans coordonnées) : pastille groupée = horizontale 92×22, 2 icônes +
compteur + « 45min » visibles ; popup = lignes cliquables. Compile OK,
0 nouvelle erreur. Rendu carte à confirmer en prod (jobs géocodés).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:23:42 -04:00
louispaulb
c05a6272b7 feat(ops): pickups = ~5min jobs + generic visit icon (handyman, not bolt)
Retours terrain :
1. « Ramassage d'équipement » = simple collecte chez le client → job court.
   Détection (isPickup : titre/type ~ /ramm?ass|récupération/) → durée
   plafonnée à 5 min : jobDur(pickup)=5/60 (maths d'occupation/Suggérer) +
   capPickups() sur les 3 chargements du pool (est_min affiché=5). Évite de
   surallouer le temps même si l'est_min du ticket dit plus long.
2. Icône de visite GÉNÉRIQUE (type non reconnu) : `bolt` (éclair, peu
   parlant) → `handyman` (technicien + outils). Fallback de skillIcon →
   s'applique partout (carte markerIcon + listes skillSym).

Vérifié live (données réelles) : ticket « Rammassage Équipements #253602 »
(St-Anicet) affiche ≈ 5min ; carrousel : plus de bolt, handyman présent
pour le job générique ; 0 nouvelle erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:09:22 -04:00
louispaulb
cfc721e598 feat(ops): installation jobs use 'router' icon (fiber box) everywhere
Sur suggestion de l'user (visuel maison+fibre) : l'icône « installation »
passe de construction/tools-ladder à `router` (le routeur/ONT posé au
domicile) — plus parlant pour une installation fibre. Appliqué PARTOUT via
les 2 fonctions d'icône : markerIcon (carte) + skillSym (carrousel,
kanban, blocs jour/semaine). Ligature material `router` → rend aussi bien
dans le <span material-icons> des marqueurs carte que dans les q-icon.
(Le glyphe custom maison+fibre est un SVG → impossible sur les marqueurs
carte en DOM brut ; router = meilleur équivalent material intégré.)

Vérifié live : carrousel affiche router pour les 8 jobs d'install (à côté
de build/live_tv/bolt), 0 nouvelle erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:01:01 -04:00
louispaulb
01ba6a70ef feat(ops): unassigned map jobs show job-type symbol (like assigned pills)
Les jobs non assignés étaient de simples gouttes oranges SANS symbole,
alors que les assignés ont une pastille horizontale avec icône du type de
job (📞/🔧/) + n°. Uniformisé en RÉUTILISANT le rendu éprouvé des arrêts
assignés (≠ tentative q-icon/SVG précédente qui rendait vertical/vide) :

- RouteMap.renderPins : pastille `.rm-pill` HORIZONTALE + icône via
  <span class="material-icons"> (ligature material fiable), couleur =
  priorité (orange = non assigné), SANS numéro d'ordre. Fini la goutte.
- routeUnassignedPins fournit `icon: markerIcon(required_skill||service_type)`
  (markerIcon = ligatures material, celles qui s'affichent hors q-icon).

Vérifié : le markup exact injecté dans la page rend une pastille
HORIZONTALE 30×22 avec glyphe material 14px visible (police « Material
Icons ») — plus le bug vertical/sans-icône. Rendu carte identique à celui
des pastilles assignées (mêmes classes/mécanisme). À confirmer à l'œil en
prod (dev sans jobs géocodés → 0 marqueur).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 16:54:24 -04:00
louispaulb
c0695f7ea9 feat(ops): saveable tech groups in Suggérer + solo tech route on chip click
1. Groupes de techs enregistrés (Suggérer) : sélection manuelle de techs
   sauvée comme groupe nommé réutilisable (ex. Installateurs, Épisseurs) —
   utile quand la compétence déduite du job est imprécise. Persisté par
   utilisateur (useUserPrefs 'techGroups', suit l'usager entre appareils).
   Chips « Groupes : » (clic = appliquer, ✕ = supprimer) + bouton
   « Enregistrer » (nomme la sélection courante) + chip « Tout
   désélectionner ». Complète le sélecteur par compétence existant.
2. Vue Tournées — clic sur un chip technicien = ISOLER sa tournée sur la
   carte (masquer les autres) pour voir une route précise ; re-clic sur le
   tech déjà seul = tout réafficher ; chip « Tout afficher » si masqués.
   (Avant : le clic masquait/affichait un tech à la fois — inversé.)

Vérifié live : groupes save→apply→delete + « Tout désélectionner »
(13→0) OK ; 0 nouvelle erreur. (Les chips de tournée n'apparaissent
qu'avec des jobs géocodés — comportement solo vérifié par la logique,
à confirmer à l'œil en prod.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:17:03 -04:00
louispaulb
9b866ec3b9 Revert "feat(ops): unassigned map jobs reuse assigned-stop pill + overlap fan"
This reverts commit 80e09c87ba.
2026-07-05 08:05:03 -04:00
louispaulb
1b095ae0e8 Revert "fix(ops): unify job icons — custom skillSym SVGs on map markers too"
This reverts commit e149b252b3.
2026-07-05 08:05:03 -04:00
louispaulb
e149b252b3 fix(ops): unify job icons — custom skillSym SVGs on map markers too
Les jobs ASSIGNÉS (marqueurs carte, HTML brut) utilisaient markerIcon =
ligatures material génériques (install→construction, monteur→camion
générique), alors que les jobs NON assignés (carrousel, blocs, gouttes)
utilisent skillSym = icônes SVG custom (échelle+outils, camion-nacelle,
casque). Résultat : même compétence, icône différente selon jour passé
(assigné) vs à venir (non assigné). Unifié sur les SVG custom :

- RouteMap : les marqueurs (arrêts assignés ET gouttes non assignées)
  rendent l'icône via un VRAI <q-icon> (h+render) → gère les SVG custom
  skillSym comme le reste de l'app ; repli material-icons si échec ;
  démontage propre dans clearMarkers/clearPins (pas de fuite).
- PlanificationPage : les arrêts (Tournées + revue Suggérer) passent
  skillSym(skill) au lieu de markerIcon ; markerIcon retiré (mort).

⚠️ Rendu carte NON vérifiable en préview (jeu dev sans coordonnées → 0
marqueur). Compile OK, carrousel (25 chips) inchangé, 0 nouvelle erreur.
À confirmer à l'œil en prod (jobs géocodés). Reste : unifier la FORME des
puces (chip/pill/carte) selon contexte — passe design dédiée.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 07:25:56 -04:00
louispaulb
4c8242eccb fix(ops): job overdue = due date, not created; day-list from carousel
Retours terrain :
1. « SLA dépassé » / en-retard se basait sur la CRÉATION du ticket
   (jobSlaBadge → creation), ce qui marque en retard des jobs planifiés
   plus tard (le client peut demander une visite quelques jours après
   l'incident). Désormais l'échéance = la DATE DE VISITE PRÉVUE
   (scheduled_date) : badge « En retard » seulement si scheduled_date <
   aujourd'hui ; futur/aujourd'hui = à l'heure. Repli SLA-création
   uniquement pour les jobs SANS date planifiée (backlog). (Le dispatch
   utilisait déjà scheduled_date pour le périmètre — inchangé.)
2. Le carrousel « N à répartir » (Tournées) est plus long à scanner que la
   liste par jour du legacy. La pastille « N à répartir » est maintenant
   cliquable → ouvre la liste VERTICALE des jobs de ce jour (panneau
   « Jobs à assigner » via openDayInPanel, filtré sur le jour) avec
   recherche + chips type/compétence + tri par ville. Meilleure lecture,
   surtout sur mobile.

Vérifié live (Tournées, 06/07) : clic « à répartir » ouvre le panneau
filtré (25 jobs du jour) ; 0 nouvelle erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 07:13:38 -04:00
louispaulb
bae7303d66 fix(ops): Suggérer never sweeps overdue jobs unless opted in
Le correctif précédent ne couvrait que l'entrée Tournées. Les jobs en
retard (scheduled_date < fenêtre) ou sans date étaient encore balayés sur
le jour choisi via d'autres chemins (bouton SUGGÉRER hors Tournées,
solveur  Optimiser). Corrigé À LA SOURCE + au placement :

- suggestJobs() (source unique — utilisée aussi par le solveur qui reçoit
  job_names) : exclut désormais les jobs hors fenêtre (retard/sans date/
  autre jour) SAUF si l'utilisateur coche «  en retard » / « Sans date »
  ou fait une sélection lasso. Sélectionner un seul jour ne dispatche donc
  QUE les jobs dus ce jour-là.
- buildSuggestion() : la branche fourre-tout `else cand = win` ne place
  plus les retards/sans-date automatiquement — même garde (lasso ou chips
  explicites) en défense.

Vérifié live (Tournées, 06/07) : plan de 25 jobs, tous sur « 07-06 »
(aucune date antérieure), 0 nouvelle erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 00:16:19 -04:00
louispaulb
8b3c97de9d fix(ops): Suggérer no overdue-sweep in Tournées + mobile review scroll
Deux retours terrain :
1. Jobs « en retard » (date < jour sélectionné) apparaissaient dans la
   répartition auto. Cause : en vue Tournées, suggestWindow suivait les
   chips de date du POOL, pas le jour choisi dans la bande → buildSuggestion
   balayait les jobs antérieurs sur le jour (branche `else cand = win`).
   Fix : en vue Tournées, `suggestWindow = [routesDay]` (la bande de dates
   fait autorité) → seuls les jobs DUS ce jour-là sont répartis, jamais les
   retards. (Le balayage des retards reste dispo hors Tournées via les chips.)
2. Revue « Répartition suggérée » trop dense sur mobile, lignes rognées
   sans défilement. Fix : dialogue MAXIMISÉ <sm + chaque rangée job
   (.suggest-entry) défile horizontalement (nowrap + overflow-x, enfants
   non rétrécis, sujet borné) → toute la ligne (badges/AM-PM/actions)
   accessible au swipe.

Vérifié live 375px : dialogue plein écran ; 25/25 rangées défilent
(scrollW ~698 > clientW 341) ; plan bâti sur le jour choisi ; 0 nouvelle
erreur console.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:42:25 -04:00
louispaulb
80e09c87ba feat(ops): unassigned map jobs reuse assigned-stop pill + overlap fan
Sur la carte Tournées, les jobs NON assignés (props.pins) étaient de
simples gouttes oranges sans icône ni gestion du chevauchement, alors que
les arrêts ASSIGNÉS sont des pastilles round-rect (icône compétence + n°)
qui s'éclatent en éventail au survol quand elles se chevauchent
(recluster/fan). Uniformisé :
- renderPins() rend la MÊME pastille .rm-pill (icône de compétence via
  pin.icon = skillSym), variante « non assigné » (.rm-pill-un : bordure
  pointillée + halo orange), avec hover onEnter/onLeave.
- recluster() opère désormais sur stopMk.concat(pinMk) → arrêts et jobs
  non assignés participent au même éclatement au chevauchement.
- routeUnassignedPins fournit `icon` (skillSym du required_skill/service).

⚠️ Visuel (pastille + éventail) NON vérifiable en préview locale : le jeu
de données dev n'a pas de coordonnées sur les jobs → 0 pin rendu. Code =
miroir fidèle du rendu d'arrêt éprouvé ; map s'initialise, 0 nouvelle
erreur console. À vérifier à l'œil en prod (adresses réelles géocodées).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:24:19 -04:00
louispaulb
cef6507f5f feat(ops): filter chips scope Suggérer + skill-matched techs (Tournées)
Suite au carrousel : les chips de filtre de la bande « à répartir »
pilotent maintenant AUSSI « Suggérer » (« ce que je filtre = ce que je
répartis »).

- Chips PRIORITÉ (normalisée high/medium/low, via POOL_PRIOS) ajoutés à
  côté des secteurs ; filtrent la liste + la carte (routeDayUnassignedView
  = jour + compétence + secteur + priorité). Affichés seulement si >1
  priorité présente (comme les secteurs).
- suggestJobs() : en vue Tournées, le périmètre = routeDayUnassignedView
  EXACTEMENT (corrige l'incohérence : avant, les chips secteur étaient
  ignorés par Suggérer alors que l'infobulle disait « du secteur »).
- openSuggest() : pré-sélection des techs par COMPÉTENCE — parmi les
  disponibles (quart ce jour), ceux dont t.skills couvre un required_skill
  des jobs du périmètre ; repli sur tous les dispos si aucun match.
  Toujours modifiable (le dialogue liste les 56 techs, cochables).

Vérifié live (06/07) : secteur Huntingdon → vue 4 jobs → Suggérer affiche
« 4 job » (avant : ignorait le secteur) + 13/56 techs pré-cochés
(compétents+dispos, reste modifiable). 0 nouvelle erreur console.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:11:02 -04:00
louispaulb
b7b1647ece feat(ops): unassigned-jobs strip → horizontal carousel (Tournées)
La bande « N à répartir » sous le sélecteur de jour (vue Tournées)
retournait à la ligne sur 2 rangées (max-height 70px, scroll vertical) et
PLAFONNAIT à 16 jobs (« +N de plus » cachait le reste). Passée en
carrousel horizontal 1 rangée (flex nowrap + overflow-x auto, scroll-snap,
barre fine) qui montre TOUS les jobs du jour — compact au-dessus de la
carte, tactile/tablette, aligné avec la bande de dates au-dessus.

Vérifié live (Tournées, 06/07 = 25 à répartir) : 25 chips rendus (fini le
plafond 16), 1 rangée nowrap, scrollWidth 5870 > 1214 = défile bien.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:03:51 -04:00
louispaulb
1017cab649 refactor(ui): T1 audit — dédup formatters → useFormatters (0 changement d'affichage)
3 formateurs canoniques ajoutés (initials [2 premiers mots, '?'], fmtClock [«8:30»→«8h30»], formatDateLong)
+ remplacement des copies locales IDENTIQUES (vérifiées à l'impl) :
- initials : PlanificationPage + RouteMap
- fmtTime → fmtClock : JobCard + TechTasksPage
- formatDate → formatDateLong : ReportRevenueExplorer + ReportInternetCher
Les VRAIES variantes laissées telles quelles (staffInitials 1er+dernier ; fmtDate string ; fmtDur RendezVous ;
fmtMoney multi-devise ; formatDate Settings/ConversationPanel/Gifts date+heure). Build vert, leak 0.
Première tranche de l'audit workflow 26 pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:31:18 -04:00
louispaulb
112ed0ea2b fix(ui): fiche client — en-tête ne déborde plus en mobile ; assistant = sélecteur recherchable
- CustomerHeader : sous-ligne (nom/legacy/type/langue) était `no-wrap` → débordait sa colonne et
  chevauchait le bloc statut/liens en mobile. Fix : wrap + colonne `min-width:0` + le bloc statut/ERPNext/Users
  passe pleine largeur en dessous (<sm) via `col-12 col-sm-auto` ; titre en ellipsis.
- Ajouter un assistant : q-select brut → <TechSelect> (recherche typeahead) ; jdTeamOptions = nom · compétences
  (searchable par nom OU compétence), CAPABLES d'abord (possèdent la compétence du job), value = id ; jdAddAssistant résout le tech par id.

Vérifié préview mobile (375px) : sous-ligne wrap, 0 débordement (scrollW==clientW) ; build vert, leak 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 17:31:44 -04:00
louispaulb
80c8005f0c feat(planif): symbole de compétence du job sur les chips « à répartir » (Tournées)
Remplace l'icône terrain/distant par le SYMBOLE de compétence (skillSym : live_tv/build/cable/cell_tower/camion-nacelle…, couleur = getTagColor) — cohérent avec les cartes kanban + blocs de tournée. Vérifié préview : chips = live_tv (TV), build (réparation)…

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 10:16:12 -04:00
louispaulb
c8afa97004 feat(planif): Tournées — chips secteur + jobs non assignés sur la carte (gouttes), filtre liste+carte
Batch D — regrouper/situer les non-assignés du jour :
- RouteMap : nouvelle prop `pins` (+ renderPins/clearPins/watch, gouttes oranges couleur=priorité, clic → @pin-click) — miroir de `live`, inclus dans le fit bounds
- Planif Tournées : chips SECTEUR (par ville, comptés, triés) qui filtrent SIMULTANÉMENT la liste « à répartir » ET les gouttes sur la carte ; routeUnassignedPins (coords robustes latitude/lat), clic goutte → détail ; « Suggérer » agit sur le secteur si filtré

Vérifié préview : jour 06/07 → 10 secteurs + 21 gouttes ; clic « STE CLOTILDE » → 4 chips + 4 gouttes (« 4 à répartir / 21 »).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 10:06:34 -04:00
louispaulb
21724e65f8 feat(planif): Tournées — filtre compétence sur la carte + jour ciblé + jobs non assignés en évidence
Batch B — le filtre de compétence pilote la carte : dayLivePositions (GPS techs) filtré par visibleTechs
(les tournées l'étaient déjà via visibleTechs) → seuls les techs capables + leurs jobs sur la carte.

Batch C — mise en évidence des non-assignés du jour :
- Badge rouge « dus non assignés » par jour sur la bande (OccupancyStrip.os-unassigned), depuis unassignedByDay
- routeDayUnassigned = jobs dus le jour sélectionné mais non assignés → bande « N à répartir » (chips cliquables → détail) + bouton « Suggérer » ; se met à jour au clic sur la strip

Vérifié préview : positions filtrées par compétence ; badges [1,21,16,15,3,8] sur la bande ; clic 06/07 → bande « 21 à répartir » (14 chips réels).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 09:58:58 -04:00
louispaulb
012f924683 feat(planif): simplification barre du haut — Suggérer (jour sélectionné), fix recherche X, retrait Équipe
- Bouton « Générer » (haut) → « Suggérer » (openSuggest) basé sur le jour de la vue ACTIVE (strip Tournées / Jour kanban / bande mobile). Le solveur de quarts est déplacé dans Outils (« Générer l'horaire (semaine) », doGenerate conservé).
- Fix recherche tech : le X (clearable) mettait search=null → `search.value.trim()` plantait le computed visibleTechs → le champ « ne se vidait pas ». Rendu null-safe.
- Retrait du sélecteur « Équipe » (groupOptions vide : aucun tech n'a de `group` ; le filtre par compétence suffit).

Vérifié préview (desktop 1400px) : X vide le champ sans planter, Suggérer ouvre le dispatch auto, « Équipe » absent, grille rendue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 09:47:42 -04:00
louispaulb
b7c24f58b2 feat(planif): sélecteur de jour « Tournées » = 2 semaines par défaut
routeStripDays passe de 7 → 14 jours (depuis le lundi affiché), données via /roster/capacity
(14 j, découplé de la fenêtre de charge 7 j du grid → barres réelles en semaine 2). Chargé à
l'entrée de la vue Tournées + au changement de semaine (watch [boardView, start]). Bande pleine
largeur + défilement horizontal ; jour courant marqué.

Vérifié préview : 14 cellules (29/06→12/07), today marqué, scroll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:55:29 -04:00
louispaulb
5491aa8abe feat(dashboard/planif): vue 2 semaines + deep-link jour + slot-finder « Planifier des quarts »
- Tableau de bord : 2 bandes d'occupation (« Cette semaine » / « Semaine prochaine », 14 j depuis lundi), jour courant marqué (liseré indigo dans OccupancyStrip). Clic jour → /planification?day=ISO
- Planif : deep-link `?day=` (cadre la semaine + sélectionne le jour grille/mobile/tournées) & `?skill=` (filtre compétence, match casse-insensible vs allSkills) via focusDay() lu au montage
- Slot-finder (SuggestSlotsDialog) : état « Aucun créneau » → bouton « Planifier des quarts pour « X » » → émet plan-shifts → Planif ouvre la timeline (vue grille) au jour cherché, filtrée sur la compétence, + toast « crée un quart pour ouvrir des créneaux »

Vérifié préview bout-en-bout : 2×7 jours + today, clic → ?day=2026-07-09 cadre 06→12/07, recherche vide → bouton → dialog fermé + grille + toast compétence. Build vert + leak-check 0. Aucun changement hub.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:34:43 -04:00
louispaulb
9a507d35db feat(dashboard): bande d'occupation + KPI cliquables/tendance + garde-fou PPA ; barres de charge en-tête Planif
- <OccupancyStrip> réutilisable (extrait de .pm-strip) ; heatColor déplacé dans useHelpers = source unique
- Tableau de bord : bande « Charge à venir » (7 j glissants, réutilise /roster/capacity) ; clic jour → Planif ; légende + heures à répartir
- KPI cliquables (→ page liée) + sous-ligne tendance/contexte (urgents, +N tickets 7 j, non assignés)
- Garde-fou PPA : confirmation explicite avant prélèvement (cf. incident double-charge 2026-06)
- Planif : barres verticales d'occupation dans l'en-tête desktop (hdr-vbar) + sélecteur de jour « Tournées » = OccupancyStrip (remplace les chips « lun 29/06 »)

Vérifié en préview (strip, clics jour, dialog PPA annulé sans charge, nav KPI) + build vert + leak-check 0. Aucun changement hub (réutilise /roster/capacity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:00:46 -04:00
louispaulb
cf9feb67f4 refactor(ops): retirer la page Dispatch dépréciée — tout sur Planification (−2179 lignes, −240 KB)
Batch 4 de l'audit. La page Dispatch n'était plus utilisée ; Planification la remplace.
- Supprimé DispatchPage.vue (2179 lignes → chunk 240 KB disparu du build).
- router : route /dispatch → REDIRECT vers /planification (anciens signets OK).
- nav.js : entrée « Dispatch » retirée. PlanificationPage : 2 liens « Tableau Dispatch » retirés.
- Dashboard (clic job du jour) + TaskNode (goToDispatch) → redirigent vers /planification.
- src/api/dispatch + src/config/dispatch INCHANGÉS (modules partagés app-wide).
Vérifié : build OK (chunk DispatchPage disparu), nav sans Dispatch (26 items), Planif rend,
/dispatch redirige.

NON appliqué (vérif avant édition = l'audit ne tenait pas) : consolidation initials()
— PAS équivalent (staffInitials = 1re+DERNIÈRE initiale + fallback '' ; local = 1re+2e mot +
fallback '?') → changerait les initiales affichées. Fixes de fuites : composants ont déjà
onUnmounted → « fuites » = cas v-show non confirmés. Laissés tels quels (churn/risque > gain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:03:57 -04:00
louispaulb
1af8c62142 feat(planif): géofencing des jobs — timeline En route/Arrivé/Reparti (suivi façon colis)
Le hub compare la position GPS live de chaque tech (Traccar) aux jobs du jour →
détecte les transitions et les journalise par job. Le statut « live » du job est
DÉRIVÉ du journal (compute-on-read, aucune écriture ERPNext — pas de valeur « en
cours » côté doctype). Fiche job : timeline horizontale Assigné → En route → Arrivé
(HH:MM) → Reparti (HH:MM).

- lib/geofence.js : runScan() (rayon 150 m, sortie 230 m hystérésis, séjour 3 min),
  store data/geofence.json par job, machine à états ; startScan() scheduler 90 s
  (GEOFENCE_SCAN=off pour couper). Vérifié prod : 12 jobs × 14 positions, transitions OK.
- server.js : démarre le scan au boot.
- roster.js : GET /roster/job/:name/geofence (timeline) + /geofence-states + /geofence-scan (test).
- api/roster.js : jobGeofence(), geofenceStates().
- PlanificationPage : openJobDetail charge la timeline ; stepper .jd-geo (scoped).

Suite possible : écrire le vrai statut ERPNext à l'arrivée (nécessite d'ajouter une
valeur « En cours » au doctype Dispatch Job) — non fait pour éviter le risque schéma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:34:55 -04:00
louispaulb
d966e1b749 feat(planif): positions GPS live des techs (Traccar) sur la carte des tournées
Onglet Tournées : marqueurs « live » de chaque tech (appareil Traccar associé),
rafraîchis ~25 s tant que l'onglet est ouvert. Marqueur = pastille ronde à initiales,
couleur = celle de la tournée du tech, halo pulsé si position fraîche (grisé sans halo
si > 10 min). Tooltip : nom · « il y a X min » · vitesse (km/h) ou « arrêté ».
Bouton « GPS live (N) » pour afficher/masquer. Respecte les chips techs masqués.

- hub roster.js : GET /roster/tech-positions → {positions:[{techId,techName,lat,lon,time,speed}]}
  (batch getPositions par appareil). Vérifié prod : 14 positions.
- api/roster.js : techPositions().
- RouteMap.vue : prop `live` + marqueurs .rm-live (halo pulsé, staleness), watch dédié.
- PlanificationPage : polling 25 s (démarré sur l'onglet Tournées, arrêté sinon +
  onUnmounted), couleur mappée depuis dayRoutes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:06:16 -04:00
louispaulb
c74462f97b feat(planif): dispatch auto = SEULEMENT les jobs du jour sélectionné (retards exclus par défaut)
Sélectionner le 7 juillet ne devait pas tirer les jobs en retard/sans date (souvent
faits mais pas fermés) dans la simulation. Par défaut (aucun chip de date coché),
suggestJobs ne garde que les jobs DATÉS sur le(s) jour(s) de la fenêtre — plus de
pollution par les retards/autres jours. Pour dispatcher les retards : cocher le chip
«  en retard » (comportement inchangé). Le job_names envoyé au solveur hub ne contient
alors que les jobs du jour → même résultat côté Optimiser.

Vérifié preview : jour 07-07 → 15 jobs, plan = 15 arrêts tous le 07-07, 0 en retard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:56:56 -04:00
louispaulb
8d80b44a79 feat(planif): « Suggérer » respecte aussi le filtre de TYPE du pool
Étend le filtre du dispatch auto au type affiché (« ce que je vois = ce que
je répartis ») : suggestJobs part désormais de assignJobsFiltered (type + date)
au lieu des seules dates. Défaut inchangé (aucun filtre = tout le pool part).
Levier concret : décocher « téléphonie » → non réparti aux techs terrain.

- suggestJobs : base = assignJobsFiltered (type ∩ date) hors On Hold ; lasso prioritaire.
- suggestFiltered + suggestScope : bouton « Suggérer (N) » et dialogue
  « N job(s) des jours + types cochés → jours » quels que soient les filtres actifs.
- Aucun changement hub : optimize-plan filtre déjà par job_names (déployé).

Vérifié preview : réparation seul → 18 ; réparation + 07-06 → 5 arrêts,
tous réparation, tous le 07-06 (heuristique locale).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:09:18 -04:00