Logique métier : c'est le job précis qui impose la compétence/niveau, pas une
caractéristique globale. Retrait du bouton + éditeur « Niveau requis par compétence »
(level_by_skill/level_by_type globaux) et du repli global dans le moteur → le niveau
vient UNIQUEMENT du job (required_level persistant). La gestion compétences+niveaux se
fait PAR TECHNICIEN dans le Suggérer via l'icône ✏️ = TagEditor (chips + ★), le MÊME
composant que le tableau timeline (Dispatch). Sous-compétences (ex. émondage) = simples
compétences/tags créables et assignables au tech.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Niveau requis PAR JOB persistant : store hub durable (job-levels.json) + endpoint
POST /roster/job-level + buildUnassigned enrichit required_level ; pastille « niv »
écrit via l'API (survit reload/session). NB : l'API Custom Field ERPNext v16 échoue
(IndexError) sur ce doctype custom → store hub (migratable en champ ERPNext plus tard).
- Lasso FREEFORM : tracé libre (souris + tactile) → polygone SVG + point-in-polygon sur
les pins/amas projetés (remplace le rectangle) + expansion des amas (getClusterLeaves).
- D v2 : carte des tournées = routes ROUTIÈRES réelles (Mapbox Directions par tech,
domicile→arrêts) + distance/temps RÉELS dans la légende (cache par signature ;
fallback segments droits + estimation haversine).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Sécurité : sanitize DOMPurify sur tout HTML entrant courriel/osTicket (v-html) →
ferme le vecteur XSS (ConversationPanel + IssueDetail). Nouveau src/utils/sanitize.js,
dompurify ajouté en dépendance directe.
- Fix : piège TDZ (watches de la carte des tournées placés avant les const suggestDlg/
suggestMapDay/suggestGroups) qui cassait le montage de PlanificationPage → déplacés
après les définitions.
- Finition : étiquette de fenêtre du dispatch auto « Aujourd'hui + Demain » (au lieu de l'ISO).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).
Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Comms: sélecteur d'expéditeur « De: » (défaut groupe Support TARGO) via resolveSendFrom + alias vérifiés
- Notifs: prefs de feeds PAR utilisateur (/conversations/notif-prefs) + cloche à bascules ; boot tooltip-ux (clic prioritaire + anti-empilement)
- Courriel: invitation à évaluer = modèle Unlayer éditable (transactional-rating-invite-*) ; test-send via Gmail + expansion {{rating}} ; logo TARGO auto-hébergé sur le magasin d'actifs du hub
- Planif: bloc « sans déplacement » (damier, début de quart, alerte si pas de quart), quart éditable dans l'éditeur de jour, icônes de compétence en vue jour (TV pour télé), clic cellule → éditeur, clic gauche lane → liste / clic droit → menu quart, lien ↗ ticket par job
- Tickets: défaut « Non fermés » + correction du filtre « Mes tickets » (owner)
- Inbox: poll Gmail 1 min + rafraîchir à la demande (poll-now)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Email posts now render at FULL content height (fitFrame cap raised 1800→20000 guard)
so there is no per-message internal scroll — the whole thread scrolls as one.
- The live draft mirror ("X rédige une réponse…") gets a close X (closeOtherDraft):
dismiss another agent's in-progress draft preview; reset on conversation switch so
a genuinely new draft re-appears.
Verified: a 60-paragraph email renders at 2681px (no inner scrollbar); the mirror
shows a close button that hides it on click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Email messages now size to their own content (fitEmailFrame): iframe sandbox gains
allow-same-origin (NO allow-scripts — scripts stay blocked, parent just measures
body.scrollHeight) and the height is set on @load + re-measured on expand. Removes
the fixed 300px frame and the "fill empty space" stretch (no more big whitespace).
msg-body is now v-if (renders on expand) so the measure runs while visible + lazier.
- Reply arrow in each CUSTOMER message header → opens + focuses the composer without
scrolling to the bottom button.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sender identity on outbound:
- Replies/new emails now go out as e.g. "Gilles Drolet" <support@targo.ca> instead
of the generic alias name "Service TARGO". The name is derived from the agent's SSO
email (gilles.drolet@ -> "Gilles Drolet"); the ADDRESS stays the monitored alias so
replies still land in the shared inbox. Verified: Gmail honors the custom display
name on the send-as alias (sent From header = "Gilles Drolet <support@targo.ca>").
- gmail.sendFrom() exported; conversation.js agentSendFrom() builds the From from the
stamped msg.agent (replies) / x-authentik-email (new sends via email-new).
UI cleanup (space + clarity, Gmail-style):
- One thin separator line between messages; removed the rounded gray card box around
emails (.email-card flat), the email-card-head bar, and the blue expanded-head
highlight.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On mobile the conversation full-page used a fixed height + overflow:hidden, and
the reply q-editor had no max-height — a long body/signature grew it unbounded
and pushed the send button past the clipped bottom (user had to shorten the email).
- q-editor: max-height=40vh → content scrolls internally, toolbar + send row stay put.
- ConversationFullPage @media (max-width:700px): height auto + min-height 100dvh +
overflow visible → the page scrolls vertically instead of clipping.
Verified at 390x844 with a 27-line signature: editor caps at 40vh (overflow auto),
"Envoyer le courriel" stays in view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inbox identity/grouping (fixes mislabeled threads):
- customerName = real From display name, then a customer matched BY EMAIL,
then the raw address — never the AI's content-guessed name (a gilles@ relay
showed as "Sylvie Juteau").
- never group a thread by one of our own domains (support@targo.ca,
*@targointernet.com): re-ingested outbound was collapsing unrelated threads
into one mislabeled "Guylaine Gagnon" thread.
Refactor: queues/TYPES/QUEUE_OF/CAT centralized in lib/categories.js (were
drifting across conversation.js + inbox-triage.js; telephonie/television had
no matching triage type). Removed dead export findConversationByEmail.
Feat: addMessage stamps msg.agent on outbound replies -> per-agent stats.
Historique (/historique, HistoriquePage.vue): tournées par technicien (ALL
statuses incl. Completed/Cancelled — the board hid them), tech leaderboard,
inbox leaderboard. Hub: /dispatch/history, /dispatch/leaderboard,
/conversations/leaderboard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hub (lib/roster.js, vision.js, legacy-dispatch-sync.js, server.js) + Ops + pont legacy.
- Capacité par jour AM/PM (Soir = réserve garde/urgence, jamais offerte) calculée
client-side sur les techs visibles -> suit le filtre de compétence.
- Modèle de durée ADDITIF (caractéristiques, tableur inline) + auto-détection
DÉTERMINISTE par mots-clés (sans IA permanente) ; est_min branché sur capacité + pool.
- Capture terrain passive : endpoints publics /field (job/tech/checkpoint/ts/photo/
device/vision), tokens HMAC signés sans PII ; dérive actual_start/end. UI hébergée
public/field-app.html (liste/carte Mapbox/Street View/photo/scan MLKit->Gemini).
- Chrono job (start/finish), repositionnement carte (set-location), vue satellite,
Street View clic-droit, année devant les dates dues groupées.
- Sync techniciens : rapport de réconciliation 3 systèmes (staff legacy / Dispatch
Technician / groupe Authentik), application MANUELLE, zéro écriture Authentik (+11 fiches).
- vision.js : extractEquipment() réutilisable (marque/modèle/série/MAC/codes-barres).
- Pont legacy (ops_reassign.php) : désassignation reflétée, fermeture ticket, retour
au pool ; notification courriel à l'assignation.
Déployé sur le hub ; ce commit aligne le repo sur l'état en production.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Panneau « Jobs à assigner » : tri/groupes par DATE DUE (≠ création) avec étiquettes
relatives (« Aujourd'hui », « MM-DD ⏰ en retard », future) + date due affichée sur chaque ligne
(overdue en orange). ASC/DESC place aujourd'hui dans l'ordre chronologique.
- Fermeture (unitaire + lot) : confirm via $q.dialog (au lieu de window.confirm) — propre + fiable.
- Carte → liste : clic sur un pin sélectionne + scrolle la ligne dans la liste + déplie son fil + flash visuel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Axe de temps (hdr-ruler 0..24) retiré des en-têtes de jour (inutile dans la grille ; encombrant).
- La bande d'occupation `.tl` ne stoppe plus la propagation (plus de @mousedown.stop/@click.stop) → le
clic/drag sur la cellule remonte à `<td>` → onCellClick (menu d'horaire) + onDown/onEnter (sélection) refonctionnent.
- L'éditeur de journée s'ouvre désormais en cliquant un BLOC de job (.tl-blk, @click.stop) — pas toute la
bande. Donc : clic sur un bloc coloré = éditer la tournée ; clic ailleurs dans la cellule = modifier l'horaire.
Le tooltip d'occupation (survol) reste actif. Curseur/hover sur les blocs cliquables.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Barre timeline : l'espace entre 2 jobs (le déplacement) est rendu en POINTILLÉS (au lieu du gris vide),
tooltip « 🚗 déplacement ». dayTravelSegs() = gaps entre packedDay[i].end et [i+1].start.
- Carte : clic sur un pin → POPUP avec détails du job (n°, sujet, heure, client) ; curseur main au survol.
- Liste : clic sur une ligne → recentre la carte (easeTo zoom 14) sur ce job, en plus d'ouvrir le détail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remplace la mini-image statique (segments à vol d'oiseau) par une carte Mapbox GL :
- Itinéraire ROUTIER réel via l'API Directions (geometries=geojson) tracé sur la carte (halo + ligne).
- Pins numérotés dans l'ordre de tournée (cercle coloré par compétence + numéro).
- Navigable : zoom molette + boutons NavigationControl (+/-), déplacement (pan), ajustée au territoire (fitBounds).
- Lifecycle : init à l'ouverture du dialogue (après anim + resize), refresh débouncé au réordonnancement
(re-trace l'itinéraire), destruction à la fermeture (pas de fuite). mapbox-gl chargé en CDN (comme le Dispatch).
- Avertissement « N sans coords » conservé. Validé : Directions OK (géométrie 392 pts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dernier recours quand l'adresse exacte est introuvable : placer le Service Location au CENTROÏDE
(rqa_addresses) de son code postal (préféré) sinon de sa ville → le job apparaît dans le bon secteur.
- Hub : applyAreaFallback() (CTE centroïdes CP/ville, index-friendly) + POST /address/conformity/apply-area.
Statut 'area', linked_address '≈ centre <CP/ville>'. Hors-QC/junk (absents de rqa_addresses) restent unmatched.
- Ops : carte stat « ≈ Secteur (CP/ville) » + bouton « ≈ Centre CP/ville (reste) » dans la page Conformité.
Exécuté : 317 placés (303 par code postal, 14 par ville) → unmatched 365 → 48 (Toronto/boîtes postales/junk).
État final : validated 16 257 · review 489 · area 317 · unmatched 48 → 99,7 % des services ont une position.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rend le mécanisme réutilisable (« faire de même pour tous les campings ») :
- Hub (address-conformity.js) : GET /address/conformity/campings (registre + nb lots par camping),
POST /campings (upsert {keyword,name,address,lat,lon} → applique direct), POST /campings/apply (réappliquer).
applyCampings() = UPDATE des lots (match ville normalisée) → géoloc fixe du camping.
- Ops (page Conformité adresses) : section « Campings — géoloc de remplacement fixe » : table du registre
(nom, adresse principale, GPS→Google Maps, nb lots) + formulaire d'ajout (nom/mot-clé/adresse/lat/lon)
qui ajoute ET applique, + bouton « réappliquer ». api/address.js : campingsList/Upsert/Apply.
→ Pour un nouveau camping : on saisit son adresse principale + GPS, tous ses lots pointent dessus (le tech
navigue au camping). Registre seedé : Lac des Pins, Dauphinais, SandySun, Frontière, Ensoleillé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Minimap Mapbox Static ajoutée sous la timeline : pins numérotés dans l'ordre de tournée + tracé reliant
les arrêts, ajustée auto au territoire des jobs → on VÉRIFIE d'un coup d'œil que chaque arrêt tombe à la
bonne adresse (un pin mal placé = coord à corriger via Conformité adresses). Clic → carte interactive (Dispatch).
Indique « N sans coords (absent de la carte) » le cas échéant. Helper encodePolyline (precision 5) pour le tracé.
- Sélecteur de priorité retiré de chaque ligne (défaut « Moyenne » conservé en donnée, géré au Dispatch) → gain de place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plus besoin de re-chercher avec un processus complexe : une page liste les adresses de service non
conformes (review/unmatched) avec leur proposition AQ canonique, et permet de RÉSOUDRE une fois (persisté) :
- Approuver : la proposition AQ devient officielle (validated, coords RQA).
- Corriger : recherche AQ locale (rqa_addresses + fibre) → lier la bonne adresse.
- GPS : saisir/coller lat,long (relevé sur map.targointernet.com qui a la géoloc des unités de camping)
+ lien direct « voir sur la carte » par ligne.
- Rejeter : pas d'adresse civique (boîte postale/hors-QC) → 'no_address'.
Tri par type (camping / civique à corriger / à confirmer / non-adresse) + stats + recherche + pagination.
Backend : lib/address-conformity.js (GET stats|list|candidates, POST resolve) sur le Postgres LOCAL,
routé /address/conformity/* (server.js). Front : api/address.js + pages/AddressConformityPage.vue + route
/conformite-adresses + entrée nav « Conformité adresses » (icône MapPinned, requires view_settings).
État courant : validated 15 195 · review 1 366 · unmatched 550 (camping 540 / civique 333 / non-adresse 93).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pont (legacy-dispatch-sync.js) :
- Import des coordonnées par job via cascade : table legacy `delivery` (point de service exact,
JOIN ticket.delivery_id) > Service Location ERPNext > géocodage RQA > géocodage Mapbox.
Validation bornes Québec (coord()). Couverture 153/172 (89%).
- Géocodage RQA corrigé : retrait du générique de voie (Rue/Rang/Chemin absent de
odonyme_recompose_normal) + code postal non accolé au terme (sinon ilike ne matche jamais).
- Repli Mapbox geocoding pour rues trop récentes pour le RQA (MAPBOX_TOKEN).
- Backfill + UPGRADE : coords delivery écrasent des coords SL moins précises (jamais l'inverse).
- Comptabilité honnête : vérifie r.ok sur create/update (erp ne throw pas) → errors + error_samples.
- Verrou de sérialisation sync() : tick + runs manuels ne se chevauchent plus (frappe_pg).
- Subject tronqué à 140 (champ Data) → corrige CharacterLengthExceededError sur jobs sans SL.
- Observabilité : coord_src tally + error_samples dans le résumé.
Ops Planification (éditeur de journée) :
- travelBetween() consulte une matrice Mapbox Matrix chargée à l'ouverture (loadDayRoute) →
temps de trajet ROUTIERS RÉELS ; réordonnancement instantané sans nouvelle requête.
Repli haversine si Mapbox indispo. Indicateur 🚗 réel vs 📏 vol d'oiseau.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FIX réordonnancement : flèches ↑↓ (fiable) + drag-drop basé sur le DROP (au lieu du splice live jittery)
- durée éditable par job en MINUTES (pas de 5, best practice précision) → persistée via reorder-jobs (duration_h)
- temps de transport estimé entre 2 jobs (haversine sur coords Service Location, 40km/h + 5min) affiché entre les lignes
→ en attendant la géoloc live (Capacitor background-geolocation, noté pour plus tard)
- hub : occupancyByTechDay renvoie lat/lon par job ; reorder-jobs accepte duration_h ; total h en pied
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- clic sur le progressbar → q-dialog ciblé sur le tech×jour (garde le contexte de la grille derrière) :
timeline visuelle (blocs colorés par compétence) + liste éditable des jobs
- réordonnancement par DRAG-DROP (dragstart/dragover/dragend → route_order) + sélecteur de priorité + Enregistrer
- retrait d'un job (✕ → hub POST /roster/unassign-job : assigned_tech null, status open → retour au pool)
- bouton « Dispatch » comme échappatoire vers le tableau complet (gotoDispatch)
- réutilise occupancy/cellBands/cellBlocks/blockStyle + reorderJobs ; best-practice détail-drawer (pas de navigation pleine page)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- réutilisation max + cohérence : le clic sur le progressbar ouvre le tableau Dispatch focalisé sur
le tech + le jour cliqué (gotoDispatch(t, d.iso)) = LE timeline éditable (drag-drop réordonner, supprimer/désaffecter)
- retire le popup cellJobsMenu (réordonner/priorité) → règle aussi le chevauchement avec l'infobulle mouseover
- (endpoint /roster/reorder-jobs conservé, réutilisable ; le réordonnancement se fait désormais côté Dispatch)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- assignSort + assignGroups regroupe/trie selon le mode (défaut = groupe parent-enfant) ;
ajout du tri par COMPÉTENCE (demandé) + date/ville/priorité (jobCity = dernier segment adresse ou « Ville | » du sujet)
- barre de tri dans le panneau (hors zone de drag) + en-tête de groupe par label ; indentation enfant seulement en mode groupe
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Blocs d'occupation = 1 job, coloré par la COULEUR DE SA COMPÉTENCE (palette skills éditable),
au lieu du dégradé vert→rouge par taux. Hub occupancyByTechDay attache skill+job à chaque bloc
(skillForJob||deptToSkill) ; blockStyle → getTagColor(blk.skill).
- Clic sur le progressbar (.tl, @click.stop+@mousedown.stop) → menu : liste des jobs du tech×jour avec
réordonnancement (flèches → route_order) + re-priorisation (select) + Enregistrer.
Hub POST /roster/reorder-jobs (route_order/priority, séquentiel) ; tri occupancy par route_order.
- Clic HORS du progressbar dans la cellule → menu shift inchangé (créer/modifier).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Couleurs liées aux skills (éditable/cohérent) : hub deptToSkill() déduit une compétence du type legacy
→ /roster/unassigned-jobs renvoie required_skill ; PlanificationPage colore la carte par getTagColor(required_skill)
(même couleur que le chip skill) ; bordure 5px
- Fil complet du ticket : hub /dispatch/legacy-sync/ticket-thread (ticket_msg + auteur staff, HTML nettoyé) ;
api legacyTicketThread ; RightPanel bouton « 💬 Voir le fil / commentaires » (chargé au clic, messages+auteurs+dates)
- Order-by du pool dispatch : useBottomPanel.bottomSort (date|city|priority) + dropdown ⇅ dans BottomPanel
(ville = 2e segment adresse / token sujet avant |)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pont : extrait le lien connect_ministra.php du ticket_msg (déjà posté par le wizard legacy à la vente)
→ champ legacy_activation_url sur le Dispatch Job (backfill des 10 tickets TV). Read-only legacy, aucun 2e
chemin d'écriture → zéro risque de double-facturation (cf. analyse processus).
- store _mapJob : expose legacyActivationUrl ; RightPanel : bouton violet « 📺 Activer STB (Ministra) »
(MÊME lien que le tech reçoit, ouvre connect_ministra.php avec tid/did/sid/cr/m intacts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- useHelpers.legacyReplyUrl(job, staffId?) → https://store.targo.ca/targo/reply_ticket.php?ticket=<legacy_ticket_id>&staff=<3301 par défaut = Tech Targo>
- RightPanel : n° ticket legacy cliquable + bouton d'action « 📝 Répondre dans legacy »
- permet au tech d'écrire dans le ticket legacy depuis ERPNext (lecture seule de notre côté)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- BottomPanel : pastille couleur (jobColor → type legacy) par ligne + badge ⏰ EN RETARD
sur les groupes de date passée (le pool est déjà groupé/trié par date)
- RightPanel : badge « en retard » près de la date planifiée (hors Completed)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pont : stocke legacy_dept (dept osTicket) sur le Dispatch Job + backfill des 70 existants
- useHelpers.jobColor/legacyDeptColor : palette comme legacy (Installation vert, Réparation or,
Télé rose, Téléphonie vert pâle, Désinstallation rouge foncé) ; tech en pause = rouge vif prioritaire
- store _mapJob : expose legacyDept + legacyTicketId
- RightPanel : champ « Ticket legacy » (#id · dept) — le client est déjà un lien cliquable vers la fiche
- doc mise à jour
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- On Hold : onCellDrop REFUSE d'assigner un job en attente d'un prérequis (notify), reste au panneau (≠ 🔒 visuel)
- Hors quart publié : marqueur ⚠ dans la cellule libre (offShiftJobs/rawCellJobs lit occByTechDay brut) +
badge « hors quart » dans la timeline ressource — surface les jobs assignés un jour sans quart
- Deep-link : Planif gotoDispatch(tech) → /dispatch?tech=&date= ; DispatchPage lit route.query
(goToDay(date+T12:00:00) anti-décalage tz + selectTechOnBoard)
- #58 : bouton « Désaffecter + aviser le client » dans le dialogue d'unassign Dispatch →
roster.notifyReschedule (désassigne serveur + SMS lien /book au mobile du Customer)
- Doc docs/features/roster.md mise à jour (Fait récemment / TODO)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Panneau « jobs à assigner » v2 : multi-sélection (cases), groupes parent-enfant
surlignés, heuristique terrain/à distance (activation/netadmin pré-décochés),
pré-total d'heures, aperçu d'occupation PROJETÉE au survol (barre fantôme + badge).
Fix barre d'occupation figée après drop : /roster/assign-job pose désormais un
start_time (premier-trou-libre dans le shift) + garantit duration_h, sinon le job
compte dans les heures mais n'affiche aucun bloc. Nouvel endpoint
/roster/backfill-start-times (idempotent) pour rattraper l'historique.
Infobulle de cellule : nb de jobs + liste triée par priorité (occupancyByTechDay
renvoie jobs[]). Timeline contextuelle par ressource (dialogue, 0 appel réseau).
Lisibilité du drag : fantôme compact semi-transparent décalé sous le curseur
(ne masque plus l'aperçu) + source estompée.
Scoring de priorité : hook proximité (neutre — secteur géré manuellement),
réservé à 20% du score quand la géoloc arrivera.
Refactor hub : helper partagé firstFitStart (assign-job + backfill).
Nettoyage : retrait code mort (onDeleteRosterTag, projUsedH), carte des sections
en tête de PlanificationPage. Doc : docs/features/roster.md + index.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Une règle a maintenant un shift SEMAINE + un shift FIN DE SEMAINE (optionnel). La rotation reste
UNE séquence (même tech sur la semaine) ; la génération choisit l'horaire selon le jour : weekend
(sam/dim) → shiftWeekend, sinon shift semaine. Wipe couvre les 2 shifts. Modèles semés:
« Garde soir 17h-00h » (17:00-23:59) + « Garde fin de sem. 8h-00h » (08:00-23:59), on_call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Les règles créées avant le refactor (techs[]+periodWeeks, sans steps/anchor) donnaient 0 assignation
(rotationTech voyait 0 étape) + anchor manquant → toujours step 0. Fix: ruleSteps() convertit techs[]+
periodWeeks en étapes {tech,weeks} (collapse des doublons consécutifs) et ruleAnchor() retombe sur une
référence stable (epoch). rotationTech/gardeSeqLabel/editGardeRule passent par ces helpers → les vieilles
règles génèrent + s'affichent correctement, sans devoir tout recréer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refonte du principe de rotation suite aux bugs (3 sem. au lieu de 2, édition non reflétée) :
- Séquence = étapes {tech, nb de semaines} ; « 2 semaines consécutives » = weeks:2 (fini les
doublons+periodWeeks qui se multipliaient). Tours inégaux = weeks différents par étape.
- ANCRAGE explicite (semaine de départ, donnée de référence stockée) : on PARCOURT la séquence
semaine par semaine depuis l'ancrage → déterministe, reflète les éditions, pas de dérive.
- Vérifié: A×2,B,C ancré 8 juin → A,A,B,C,A,A,B,C (A toujours 2 consécutives) ; réordonner reflète.
- Aperçu et génération utilisent le même parcours. Migration auto des anciennes règles (techs+period→steps).
Rappel: après édition, re-cliquer « Générer la garde » (l'horizon est réécrit, wipe ciblé du shift).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clic sur une case → touche « A » (bascule) ou menu « Marquer absent ce jour / Retirer l'absence » →
crée/supprime une Tech Availability d'1 jour APPROUVÉE (hachurée tout de suite). Multi-sélection
supportée (A marque toutes, re-A retire). Endpoint POST /roster/absence/set {tech,date,type,remove}
(remove ne touche que les absences d'un jour, pas les vacances multi-jours). Type défaut Congé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avant: « Appliquer à la semaine » n'écrivait qu'une semaine → 1 seul tech (rotation hebdo). Maintenant
« Générer la garde » matérialise la rotation sur N semaines (4/8/12/26) d'un coup, écrit direct (publié),
navigable semaine par semaine — comme un évènement récurrent. Endpoint /roster/garde/apply : wipe ciblé
des shifts de garde dans l'horizon + recréation (idempotent, reflète l'édition de la séquence). Saut
d'absent conservé. (source='manuel' car le Select n'autorise que solveur/manuel.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Plages combinables: « Soirs de semaine » (L-V) + « Fin de semaine » (S-D) en toggles (+ chips fins).
- Rotation par défaut = PAR SEMAINE (garde = bloc hebdo) → corrige la séquence brisée (par jour, un
week-end pouvait avoir 2 personnes). Index de semaine continu dans le temps → ordre respecté en
naviguant. Séquence [A,A,B] = A 2 semaines consécutives, puis B. (Sélecteur jour/semaine conservé.)
- APERÇU live : qui est de garde sur les 8 prochaines semaines, reflète la file en cours d'édition →
on voit l'ordre respecté + l'effet des modifs avant d'appliquer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avant: rotation par semaine → dans une vue d'1 semaine, un seul membre apparaissait. Maintenant la
rotation avance par OCCURRENCE de garde (occurrenceIndex = nb de jours matching weekdays depuis l'époque)
→ chaque jour de garde prend le membre suivant de la séquence (A,A,B,C…), visible sur toute la vue.
Sélecteur « Rotation : par jour de garde / par semaine » (+ N consécutifs). Défaut = par jour ;
les règles existantes (sans unit) basculent en par-jour. Saut d'absent + doublons conservés.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le multi-select est remplacé par un constructeur de SUITE ordonnée : « Ajouter un tech à la suite »
(push, doublons autorisés → tours inégaux ex. A,A,B,C) ; chaque position a un select pour REMPLACER
le tech, + ↑/↓ pour réordonner, + ✕ pour retirer. Couvre rotations inégales et substitutions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix listes vides: département = champ libre optionnel (existants OU texte), liste des techs = TOUS
(plus de désactivation sur dept). Réordonnancement de la rotation (↑/↓), édition d'une règle (crayon
→ recharge dans le formulaire → Mettre à jour). Champ « Sem. consécutives / tech » (mettre 2 = un tech
fait 2 semaines de suite). Annuler l'édition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tech Availability gagne long_term (Check). Case à cocher « Longue durée » dans le dialogue Congés.
absencesByTechDay encode « (longue durée) » → applyTemplate force « à remplacer » (pas juste sauter
comme des vacances), même si l'absence ne couvre pas toute la semaine. Complète l'intelligence
permanent vs vacances (avant: déduit de « absent toute la semaine »).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>