Converge every ticket-detail surface onto the canonical IssueDetail (in
DetailModal) + TicketCard, per reference_ticket_detail_disparities.
STATUS — one model:
- IssueDetail no longer double-renders status. The green button (shared
TicketStatusControl) is relabelled "Reporter" with a schedule icon — it is
a postpone/snooze + quick-close control, not a 2nd status setter. The single
5-value status select (Open/Replied/On Hold/Resolved/Closed) is canonical.
TAGS — one source, via the shared TagEditor:
- New useTagCatalog composable unifies the tag catalogue: ERPNext "Dispatch Tag"
∪ roster technician skills (hub /roster/technicians). Roster skills like
"sans-fil"/"épissure" now appear on ticket tags (were missing before).
- IssueDetail replaces its bespoke q-select + chip row with TagEditor; the tag
manager (rename/recolor/delete) is delegated to the composable's CRUD.
ONE ticket-row component (TicketCard):
- LocationCard's ad-hoc per-address ticket list now renders TicketCard.
- TicketsPage mobile card mode (DataTable #item slot) renders TicketCard.
- assignee handling (assigned_staff/opened_by_staff avatars) is now consistent
across fiche, location, and /tickets.
Verified in local preview against live backend: Reporter button + single
status select, TagEditor dropdown surfacing roster skills ("sans-fil" under
"Compétence"), TicketCard rows on fiche + LocationCard, no component errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le panneau (IssueDetail dans DetailModal, ouvert depuis /tickets ou la fiche client) ne
montrait pas le tech assigné — seulement `_assign` (agents) + `assigned_tech` enfoui dans
l'arbre des tâches. Ajout d'une bannière « Technicien » en tête : chips (initiales + nom)
des techs des Dispatch Jobs liés (`assigned_tech`, déjà fetchés) + staff legacy
(`doc.assigned_staff`, ce que montrent les lignes de liste), dédupliqués ; « Non assigné »
si des jobs existent sans tech. Réutilise le `initials()` local. (1/4 de la standardisation
des détails ticket — reste en session dédiée, cf reference_ticket_detail_disparities.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le chip Cc/assistant du champ d'assignation To/Cc montre maintenant les initiales de
l'assistant dans une pastille (q-avatar), comme le chip « À » (assigné). Réutilise initials().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Simplification (panneau détail desktop) — demande 2026-07-07 :
- UN seul champ d'assignation « façon To/Cc » (fusionne « Assigner à… » + section « Équipe/renfort ») :
chips À = assigné (principal, retirable → renvoie au pool) + Cc = assistants (renfort) ;
le champ ajoute : 1er tech = assigné (À), suivants (job natif) = assistants (Cc). Handlers
jdAssignOrAssist (routage) + jdUnassign ; réutilise jdAssignTech/jdAddAssistant/jdRemoveAssistant.
- Timeline de l'assistant GRISÉE (au lieu de hachurée lilas) : .kbb-blk.assist + .tl-blk-assist
(opacité + gris) dans le kanban ET la grille semaine.
- ⚠️ RecipientSelect est COURRIEL-only (pas réutilisable pour des techs) → on réutilise le PATRON
chips+autosuggest avec TechSelect.
- Décodage entités HTML à la SOURCE du pool : capPickups décode subject/customer_name/address/
service_location/legacy_detail (deEnt) → fini « l'église » dans le pool, la feuille, l'assignation.
Vérifié : build + parse OK, 0 erreur console. ⚠️ rendu visuel du panneau détail NON confirmé en
aperçu (l'environnement d'aperçu bloquait l'ouverture du volet ce tour) — à valider sur desktop réel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L'en-tête de la feuille job affichait les champs bruts → « 424 chemin de l'église ».
Passés par deEnt() (déjà utilisé pour le panneau détail complet) : sujet, customer_name, adresse.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Incident : un employé a envoyé un courriel au client en croyant ajouter un commentaire
privé au fil du ticket. Le composeur de la feuille (jobSheet) s'ouvrait en mode « Réponse ·
courriel » (public) par défaut.
- openJobSheet : `noteMode` défaut = TRUE (commentaire INTERNE) au lieu de false (réponse client) ;
ouverture en mode réponse seulement si `{ reply: true }` explicite (aucun appelant ne le fait)
- bascule réordonnée : « Note interne » (1re, défaut) · « Répondre au client » (2e, explicite) ;
couleur d'alerte (deep-orange) quand la réponse client est sélectionnée
- bouton d'envoi : « Enregistrer la note » (interne, neutre) vs « Envoyer au client » (orange) ;
indice/placeholder clarifiés (« ajoutée au fil, jamais envoyée au client » vs « ⚠ ENVOYÉE au client »)
- répondre au client reste possible mais = choix explicite (aligne le comportement sûr déjà
présent dans le panneau détail complet : case « Envoyer aussi au client » jdReplyPublic par défaut off)
- Vérifié en aperçu sur un job AVEC courriel client : le composeur s'ouvre en « Note interne »
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Heures par tech : la grille affiche 2 semaines (days=14) mais l'avertissement comparait
le TOTAL (≈72h) à la limite hebdo (40h) → faux « surcharge ». Retiré le triangle rouge +
le style rouge ; le total reste affiché en gris avec une infobulle « sur l'étendue affichée
(N j), PAS une limite hebdo ». (Le réglage Max h/sem reste pour le solveur.)
- Colonnes : .tech-col élargie (min 240→300, max 300→360, reste sticky) ; colonnes-jour
min-width 132px → ~1 semaine tient dans la vue, la 2e semaine défile horizontalement
(.grid-wrap overflow-x déjà présent). Vérifié : table 2149px > vue 1550px → défilement,
tech-col 327px, cellules 132px, 0 avertissement rouge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Suite de la consolidation des énumérations partagées (après priorités).
- config/job-types.js (NOUVEAU) : JOB_TYPE_OPTIONS + JOB_TYPES (Installation/Réparation/
Maintenance/Retrait/Dépannage/Autre) = SOURCE UNIQUE
- élimine 3 copies : wizard-constants (ré-exporte désormais), TaskNode, UnifiedCreateModal,
flow-editor/kind-catalogs (JOB_TYPES)
- LAISSÉS volontairement (vocabulaires DISTINCTS, pas des doublons) : statuts de job
(cycle de vie ≠ actions du pool ≠ ticket ≠ campagne ≠ appareil) ; issueTypeOptions ;
jobTypes de l'app terrain (valeurs anglaises Repair/Delivery/Other — à réconcilier
séparément, risque de données)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- 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>
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>
Nouveau TicketStatusControl (doctype-conscient Issue/Dispatch Job) : Reporter +1j/+7j/date · En attente
d'un autre ticket #X · Fermer/Rouvrir. Reporter = On Hold + note datée (job = replanifie scheduled_date).
Posé sur IssueDetail (page ticket + fiche client) en remplacement des boutons Fermer/Rouvrir épars.
Prochaine étape (séquencée) : câbler sur le panneau job (Planification) + convergence des 3 vues (#27).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
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>
Le @navigate du panneau ticket routait tout vers loadModalTicket ; désormais dt==='Customer' → fiche.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
Ajoute assigned_staff à la requête + colonne Assigné (avatar initiales, couleur par tech) ;
issue_type renommé Type→Département. Distingue les tickets quasi identiques (ex. 3 Cancellation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- TicketCard : badge Département (issue_type) + technicien (assigned_staff) → distingue les
tickets quasi identiques (ex. 3 « Cancellation » = Support/Facturation/Rétention).
- migrate_tickets.py : importe assigned_staff (ticket.assign_to → nom du staff) — le rattrapage
ne le posait pas. Backfill des ~7,5k tickets assignés en F mais sans technicien côté ERPNext.
- logToLinkedTicket : direction Received si l'expéditeur = adresse destinataire du ticket
(une réponse depuis un domaine interne @targo n'est plus mal classée « Sent »).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hub (conversation.js) : getOrCreateConvForIssue (lie une Issue à une conv email) + emailTicket
(envoie dans un fil Gmail, sujet [Ticket #ISS-…] + lien direct, journalise Sent Communication).
Route POST /conversations/ticket-email (gaté Authentik). La réponse du client re-thread par le tag
sujet (TICKET_REF_RX) OU le threadId → logToLinkedTicket (déjà en place) → Communication Received
sur l'Issue → visible au panneau. notifyCustomer mémorise désormais conv.threadId au 1er envoi.
UI (IssueDetail) : bouton « Envoyer par courriel » + dialogue (À/Cc/message), « Répondre » scindé
en « Note interne » (Communication) vs courriel. Lien direct = URL du ticket.
Testé : envoi réel à louis@targo.ca sur ISS-0000250013 (channel:email, Sent Communication OK).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- richHtml() (sanitize.js) : décode entités osTicket (é, ', double-échappées &#039;)
puis DOMPurify → plus de « codes html » dans les détails.
- IssueDetail : fusionne Échanges (Communication) + Fil (Comment) en UN fil chronologique
en bulles (notes internes en ambre) ; aère la mise en page.
- Bandeau Client cherchable par nom, association/dissociation 1 clic (comme une conversation)
— le ticket portait déjà .customer mais aucun champ pour le voir/lier. Résout le cas voicemail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La 1re version cascadait compte résilié ⇒ TOUS services Annulé, ce qui contredit
la politique 2026-06-13 (statut SERVICE = vérité ; employés gardent internet gratuit
sur comptes terminés) et annulait ~842 services légitimes (dont 185k$/mo facturés
encore par F ou gratuits).
Raffinement (sync_services_incremental.py, marqueur RAFFINÉ 2026-07-06) :
- annul_ghost(compte, prix) : n'annule QUE le fantôme AVÉRÉ = compte résilié +
service PAYANT (prix>0) + F silencieux depuis >24 mois (MAX(invoice.date_orig)).
Préserve les gratuits (employés), tout ce que F facture encore, et le jamais-facturé
(ambigu → revue manuelle via /rapports/resilies-actifs).
- Phases B (création) + D (rafraîchissement) passent par annul_ghost.
- Résultat mesuré : 3013 fantômes avérés annulés, 842 réactivés/préservés,
fantômes avérés restants = 0.
Scoreboard : métrique renommée « actifs sur compte résilié — à revoir » (hub note +
libellé UI) puisque le résidu n'est plus un bug mais une liste de revue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scoreboard (console de réconciliation F ↔ ERPNext) :
- hub: legacy-sync.js scoreboard() + GET /legacy-sync/scoreboard — comptes bruts
par module (clients/lieux/abos/appareils/tickets/factures/paiements) + métrique
ghost_active_subscriptions. Devices = tabService Equipment (pas tabDevice).
Count F « abos actifs » exclut les comptes résiliés (comparaison pomme-à-pomme).
- ops: carte « Réconciliation par module » sur LegacySyncPage.vue.
Fix fantômes à la SOURCE (scripts/targo-sync/, jusqu'ici hors repo) :
- La vraie sync des services = scripts Python hôte (/opt/targo-sync/, cron horaire),
PAS le hub. svc_status() ignorait le statut du COMPTE → un compte résilié F
(status 3/4/5) dont les services restent status=1 ressortait « Actif » (F ne
cascade pas). Résultat : ~3855 abonnements fantômes recréés à chaque heure.
- sync_services_incremental.py: svc_status() rendu conscient du compte (force
'Annulé' si compte résilié) sur Phase B (création) + Phase D (rafraîchissement).
Rollout: dry-run {Actif→Annulé: 3855} → APPLY=1 → ghost=0 stable.
- Versionne aussi run.sh + sync_invoices_incremental.py + README (snapshot fidèle
de la prod ; ces scripts écrivent en base chaque heure et n'étaient pas suivis).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A (garde-fou immédiat, sans écriture) : resolveAudience exclut les clients dont le compte F
est résilié (fResiliatedAccountIds, caché 30 min) — évite d'inviter d'ex-clients tant que la
donnée n'est pas nettoyée (cf feedback_ghost_active_subscriptions). L'aperçu affiche « N
ex-client(s) résilié(s) exclus ».
Vérifié prod : « clients actifs » 7943→5943 (2663 exclus) ; « abonnement actif ≥40$ »
3102→2213 (1125 ex-clients exclus). 33/33 tests. Déployé hub.
Ne corrige PAS la donnée sous-jacente (abos 'Actif' fantômes + MRR gonflé) ni la logique de
sync — voir tâche #25 (dry-run prêt : 3873 abos / 2663 clients / ~150K$ MRR fantôme).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Le champ « Total mensuel minimum » est désactivé tant que « Abonnement actif » n'est pas
coché (le montant vient des abonnements actifs) ; il se remet à 0 si on décoche.
- L'aperçu d'audience liste maintenant les comptes SANS courriel (rattachables) avec un lien
vers chaque fiche client (/clients/:id) → permet de valider/ajouter le courriel manquant.
resolveAudience renvoie `no_email:[{customer_id,name}]` (plafonné 300) ; l'endpoint le transmet.
Vérifié prod : filtre abonnement actif + ≥40$/mois → 3102 destinataires · 284 sans courriel
(liste des 284 renvoyée avec noms + fiches). Champ montant désactivé par défaut, activé au toggle.
33/33 tests unitaires. Déployé hub + SPA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ajout au sélecteur d'audience (mode masse) :
- Type de client : Résidentiel (customer_type=Individual) / Commercial (Company) / Tous.
- Total mensuel minimum ($/mois) : somme des Service Subscription ACTIFS (monthly_price)
par client ≥ seuil. resolveAudience agrège les abonnements actifs quand active_sub OU
min_monthly>0 ; le total est renvoyé par destinataire (monthly).
Vérifié sur données prod réelles : Résidentiel 7099, Commercial 928, total mensuel ≥50$ =
2739 clients (montants agrégés réels). Champs bien peuplés (contrairement à territoire, retiré).
32/32 tests unitaires. Déployé hub + SPA. Le blast réel reste gaté.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Couleurs restreintes à la charte TARGO (noir/vert/blanc/gris + bleu en haut) sur le
courriel ET la page RSVP : bandeau supérieur bleu #1a86c7, accents/CTA verts, texte
noir/gris, fond blanc, programme en pastilles vert/bleu/gris. Retrait orange/jaune/rose/rouge.
- Admin : le téléphone saisi s'affiche sous le courriel (n'apparaissait pas avant).
- Hub : capture de l'adresse IP du visiteur à l'inscription (x-forwarded-for → socket).
- CSV enrichi = TOUTES les infos : client, n° client, ID compte, nom au dossier, téléphone,
personnes, courriel, allergies, confiance, signaux, langue, IP, créé le, répondu le.
Déployé hub + SPA 2026-07-06. Vérifié prod : palette conforme, téléphone + IP stockés
(819 555-9999 / 96.125.192.22), test envoyé OK. 21/21 tests unitaires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Titre corrigé partout (courriel, page RSVP, admin) : « Fête Targo 20 ans » →
« Targo fête ses 20 ans » (FR) / « Targo turns 20! » (EN), tagline étoffée.
- Courriel refait façon flyer : bandeau confetti + ballons 🎈 + pastille « 20 ANS »,
programme en cercles colorés (vert/bleu/violet/rose/orange), encart « places limitées »,
ligne « Merci de votre confiance au fil des années 💚 », pied targo.ca.
Déployé hub 2026-07-06 (page RSVP live = « Targo fête ses 20 ans » ; test envoyé OK).
La page RSVP et le nom en admin viennent du hub — pas de rebuild SPA requis (le repli
cosmétique dans EventsPage suivra au prochain déploiement SPA). 21/21 tests unitaires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Suite aux demandes : bouton d'envoi test (courriel par défaut = utilisateur), auto-
rattachement par courriel/nom/téléphone, et confirmation « êtes-vous un client ? » par
recoupement — SANS autosuggest (aucune fuite de PII).
Hub lib/events.js :
- matchAndValidate({token,number,email,phone,name}) : recherche AVEUGLE de candidats par
courriel/téléphone/numéro (en parallèle), le nom corrobore. « confirmed » = ≥2 signaux
concordent sur le MÊME compte ; « probable » = 1 ; « none » = aucun. Jamais bloquant, ne
renvoie jamais de donnée client. Empêche un courriel deviné de mal rattacher.
- Formulaire RSVP : ajoute Nom (requis) + Téléphone (optionnel), n° client devient optionnel.
Message de remerciement adaptatif (« ✓ on vous a reconnu » si confirmed).
- POST /events/:id/send {test:true, test_emails[]} : envoi TEST via Mailjet/Gmail (réutilise
email.sendEmail/gmail.sendMessage + inviteEmail + rsvpLink). Envoi de masse reste gaté (400).
Ops EventsPage : badge de confiance (Confirmé/Probable/À vérifier + signaux en infobulle),
dialogue d'envoi test (canal + courriel défaut = usePermissions().userEmail), CSV enrichi
(téléphone + confiance). Filtres par niveau de confiance.
Vérifié : 21/21 tests unitaires hub (cross-validation + envoi test mailjet/gmail + masse gatée) ;
page Ops rend sans erreur, dialogue s'ouvre avec courriel pré-rempli.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
À 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>
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>
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>
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>
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>
#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>
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>