Concrétise « chacun voit ses outils, pas les 24 » : bandeau personnel dont CHAQUE carte est gated par capacité —
- Ma boîte (view_clients) : notifications non lues → /communications
- Tickets sur moi (view_all_tickets|view_own_tickets) : Issues ouvertes filtrées sur _assign = mon courriel → /tickets
- Interventions aujourd'hui (view_all_jobs) : Dispatch Jobs du jour groupés par statut → /planification
Salutation + date FR localisées (America/Toronto). Données via endpoints EXISTANTS (erp listDocs + cloche), filtrage
« à moi » côté client, dégradé gracieux (« — ») si indispo. Additif (monté au-dessus de OutageAlertsPanel), styles --ops-*.
Vérifié en dev : rendu + 3 cartes + salutation nommée + 0 erreur console. Build OK, leak-clean, déployé.
Reste (différé) : atterrissage par rôle au niveau ROUTAGE (les non-managers arrivent ailleurs que sur '/') + sources
« à moi » dédiées (non-lus par canal, mes jobs par tech) au lieu du filtrage client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Même classe de bug que 187115c, sur deux flux SSE qui manquaient la reconnexion :
- TechTasksPage (app terrain) : AUCUN onerror → au restart du hub (502→CLOSED) le technicien perdait TOUT le
temps réel (nouvelles tâches, changements de statut) jusqu'au rechargement manuel. Ajout reconnexion 3 s + resync
loadTasks() à la RE-connexion (le hub ne rejoue pas les événements manqués).
- OutageAlertsPanel : au restart, tombait en repli poll 30 s et n'en sortait JAMAIS (ni retour temps réel, ni
resync immédiat). Ajout reconnexion 3 s + à la RE-connexion : arrêt du poll + fetchActive() pour rattraper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lane 3 (durcissement) : OnuActionsPanel (suspendre/rétablir/redémarrer/vitesse/remplacer/retirer — écritures réseau live,
suspendre coupe le client) était monté SANS permission dans EquipmentDetail. Désormais gaté can('manage_settings') + sorti
du bloc olt_name (garde propre serial+olt_ip → visible même sans olt_name). Bouton « Supprimer cet équipement » aussi gaté.
(Panel déjà monté depuis un travail antérieur ; ici = gating + placement.)
Lane 1c (déployé via bundle, PlanificationPage co-édité non commité) : puces géofence live En route/Arrivé/Reparti sur les
cartes Jour (kanban) + lignes éditeur de journée, via l'endpoint /roster/geofence-states existant mais jamais appelé
(geofenceStates + refreshGeofenceStates hooké dans reloadOccupancy). Lane 1a : le picker lead exclut déjà le tech assigné
+ garde 409 « déjà assigné dans F » → re-pick déjà empêché (aucun code risqué ajouté).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avant : on associait un GPS à un tech UNIQUEMENT depuis le popover du tech (vue Semaine). Ajout du sens inverse demandé :
partir de l'APPAREIL. Hub roster.js :
- GET /roster/traccar-devices : roster enrichi (appareil + tech(s) assigné(s) + unassigned + duplicate + dernier signal),
croise Traccar getDevices × Dispatch Technician.traccar_device_id. Vérifié live (45 appareils, ex. #4 → Benjamin Djanpou).
- POST /roster/traccar-device-assign {device_id,tech_id} : assigne un appareil→tech en LIBÉRANT d'abord tout autre tech qui
le détient (le champ vit sur le tech → sinon 2 techs pointeraient le même device ; le doublon est aussi signalé dans la vue).
SPA : GpsDevicesDialog.vue (q-table statut/appareil/dernier signal/technicien via TechSelect + filtre « non associés »),
ouvert par un bouton « Appareils GPS » dans la barre Tournées de Planification. api/roster : listTraccarDevicesRoster + assignTraccarDevice.
PlanificationPage (bouton + mount) = co-édité → déployé, non commité.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native detail sections (fin de la grille+desk pour les derniers doctypes cliquables de la fiche client) :
- PaymentMethodDetail / PaymentArrangementDetail / VoipLineDetail — LECTURE SEULE (aucune charge/paiement déclenché ; mot de passe SIP jamais affiché). Enregistrés dans SECTION_MAP.
Phase 2 G3 (parité écriture fibre tech-2 Raisecom, ~10 500 ONUs) :
- olt-ops.js : écritures SNMP natives (net-snmp) suspend/rétablir (VLAN 40↔666) / redémarrer / vitesse (line-profile), OIDs EXTRAITS DU DRIVER F raisecom_rcmg.php (vérifiés à la source) + SAVE 8886.1.2.1.1.0 après chaque set. Encodages olt_id (slot*1e7+port*1e5+ontid) et onu_id ((slot+32)*8388608+port*65536+ontid) — round-trip vérifié. Repli coords ONU depuis le poller SNMP live (olt-snmp.getOnuBySerial) car les coords Raisecom ne sont pas dans Service Equipment. Gaté : env OLT_RW_COMMUNITY requis (absent → run refuse proprement, 0 set), confirm+idempotencyKey obligatoires. Vérifié live : plan tech-2 refuse sans coords/RW ; run refuse ; 0 écriture réseau tirée.
- OnuActionsPanel : ajout « Redémarrer ».
Déployé prod (hub olt-ops + SPA). DetailModal SECTION_MAP (5 entrées ajoutées) = déployé mais co-édité → non commité.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- capacityByDay compte les jobs assignés SANS heure (untimed_used_h au prorata AM/PM/Soir)
- /roster/assign-job : garde 409 si le ticket F est déjà à un autre vrai tech (force pour écraser) ; SPA = dialog confirm
- mirrorAssign : start_time depuis F due_time (am/pm) + fix due_date (cur.→r.) + backfill fill-only dans watchLegacy (97% des tickets F = 'day' → le spread capacité est le fix porteur)
- pushAssignments : garde anti-clobber (conflits F skippés, force=1) + compteur conflicts
- ticket-collab : courriels de notification → deep link OPS /#/tickets?open= (fini le desk) ; TicketsPage lit ?open= ; Dashboard ouvre le ticket précis
- DetailModal + SupplierInvoices : lien desk gaté can('manage_settings')
- NOUVEAU JobMediaModule (photos tech géo-vérifiées + journal terrain) monté dans IssueDetail onsite + volet Planif ; hub /roster/job-media + /field/photo-file signé ; JobThread poll silencieux 45s
- legacy-sync emailList : strip caractères invisibles (soft hyphen U+00AD…) — fixait un 417 InvalidEmailAddress en boucle (compte 15988)
Déployé prod 2026-07-18 (hub restart + SPA /opt/ops-app), sondes live OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Suite retour Louis : plus de mode soft/hard (toggle du commit précédent retiré). Un
seul concept — un job/ticket interne qui OCCUPE la timeline (formation · réunion ·
projet · entretien) → dé-priorise le tech au dispatch. La PRIORITÉ est le curseur :
Moyenne/Basse = les urgences/réparations peuvent quand même prendre le tech ; Urgent
(high) = bloc dur, non traversé.
Front : dialogue unique « Bloquer du temps » (Intitulé · journée complète · date ·
début · durée · Priorité [défaut Moyenne] · ticket optionnel), info adaptée à la
priorité. Un seul submit `doBlockTime` → /roster/filler-job. Retiré : bascule de mode,
branche réserve, `roster.reserveTech`. Les 2 points d'entrée (bouton volet réglages +
menu de cellule) → même dialogue, relabellés « Bloquer du temps… ».
Hub (dispatch.js suggestSlots) : la traversée urgence passe du flag `job_type ===
'Réservation'` à la PRIORITÉ — `isSoftBlock` = bloc interne (Interne/Réservation) de
priorité ≤ moyenne → traversé en mode urgent ; high résiste ; les vrais jobs client ne
sont JAMAIS traversés (pas de double-booking). Ajout du champ `priority` au fetch.
(/roster/reserve laissé en place, inutilisé — retrait ultérieur.)
Vérifié : test unitaire pierce (medium/low traversé · high résiste · job client jamais)
+ aperçu (dialogue unique, priorité Moyenne, info souple, pas de toggle). Build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deux dialogues faisaient la même chose (rendre un tech indisponible pour un bloc)
avec des champs ~80% identiques, des points d'entrée différents (bouton volet
réglages vs menu de cellule) et aucune explication de leur différence → lus comme
un doublon. Seul vrai écart : bloc DUR (hors dispatch, ticket optionnel) vs réserve
SOUPLE (dé-priorise, urgences passent).
→ UN dialogue « Réserver / bloquer du temps » avec bascule de MODE (segmented
control : « Hors dispatch (bloc) » / « Urgences OK (souple) »). Champs communs
(tech · date · début · durée · journée complète · motif/intitulé) ; priorité/type/
ticket seulement en mode bloc. Soumission `doBlockOrReserve` → /roster/reserve OU
/roster/filler-job selon le mode (endpoints inchangés). Les 2 points d'entrée
(bouton « Réserver / bloquer… » du volet réglages [souple par défaut] · item de menu
de cellule [bloc par défaut, pré-rempli]) ouvrent le MÊME dialogue.
Supprimé : resvDlg + doReserve (le dialogue « Réserver » séparé). Aperçu vérifié :
bascule de mode montre/masque priorité/type/ticket + change libellé/info/bouton.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Une mise en pause indéfinie ne s'applique plus immédiatement : elle est mise en
attente (pendingPause, localStorage — comme les congés), prend effet VISUELLEMENT
(nom barré + puce « pause à publier » sur la rangée, compteur « non publié »), et
n'est COMMISE qu'au Publier — au cas où on se trompe.
- TechScheduleDialog : le toggle « Pause indéfinie » émet `stage-pause` (plus
d'écriture roster directe) ; l'état reflète serveur OU en attente (prop pendingPaused).
- Parent onStagePause : stage/dé-stage ; réactivation d'un tech DÉJÀ en pause reste
immédiate (Disponible + re-matérialisation du patron).
- Résumé de Publier (groupé par tech) : ligne « Pause indéfinie · motif · N quart(s)
futur(s) retiré(s) » + chaque quart futur listé en retrait ; en-tête « · N pause(s) ».
- doPublishConfirmed : exclut les quarts futurs (aujourd'hui→) des techs en pause de la
charge → publish-week les SUPPRIME ; applique le statut En pause ; puis relève des jobs
via l'impact IROPS existant (checkAbsenceImpact). Quarts passés conservés ; patron
récurrent intact (réactivation re-matérialise).
Vérifié en aperçu (client-only, RIEN écrit en prod) : Philippe Bourdon → puce +
nom barré ; résumé « Pause indéfinie · Arrêt maladie · 14 quart(s) futur(s) retiré(s) »
+ 14 retraits, à côté du quart de Nathan Morrisseau ; annulé + nettoyé. Build OK.
⚠️ Chemin de COMMIT au Publier (suppression quarts + pause + relève jobs) non exercé en
prod (écriture réelle) — réutilise publishWeek/pauseTechnician/checkAbsenceImpact éprouvés.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Activer + facture brouillon" button now appears ONLY when the ticket's
customer+location has En attente subscriptions (read-only /billing/activation-
preview) — defining which completed tickets trigger activation. The confirm
shows all services that would activate and the consolidated prorated total
(internet+tv+phone in one draft invoice). Preview writes nothing; commit gated
by PRORATION_WRITE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Statut" button on the ticket (distinct from the snooze "Reporter"):
Ouvert / En attente / Complété (Resolved) / Annulé (Closed). Additive — does not
touch the shared TicketStatusControl.
For a ticket with a linked open install job, an explicit "Activer + facture
brouillon" button (also offered when set to Complété) reuses the EXISTING hub
flow: POST /dispatch/job-status Completed → chain unblock → activateSubscription
ForJob → one consolidated prorated DRAFT invoice, gated by PRORATION_WRITE /
BILLING_APPROVAL_GATE (never auto-charges; billing manager edits/approves). No
new billing logic, no hub change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /auth/staff/sync-identities upserts a unified identity per internal
Authentik user, resolving Employee + Dispatch Technician and deriving email
aliases from the company email. Resolve-first: merges into an existing identity's
key and never overwrites a manual label/tech/alias (fixes Louis-Paul, whose
login louis@targo.ca ≠ employee email louispaul@); employee lookup is
alias-aware so louispaul@ finds HR-EMP-4. "Lier les identités" button in the
Staff console.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Duplicate Authentik accounts now surfaced automatically: a "⚠ Doublons (N)"
filter chip + the warning badge itself is clickable to clean up (the 7 dup
people are status=ok, so they were hidden under the default orphan filter).
- "Horaire" button on rows with a tech reuses the SAME Planification schedule
module (TechScheduleDialog: recurring template + congés + pause) via a
/planification?sched=<tech_id> deep-link — no inferior rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /auth/staff/dedupe {email}: keeps the used account (has last_login, else
oldest) and deletes ONLY never-logged-in duplicate Authentik records for that
email — safe (no sessions/history, ERPNext untouched since it's one User per
email). Console shows "Nettoyer le doublon" on flagged rows with a confirm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- resolveEmployeeForEmail + /auth/staff now resolve the Employee through the
unified identity's aliases: louis@targo.ca finds HR-EMP-4 (under
louispaul@targointernet.com). Fixes "no employee linked" for Louis-Paul.
- /auth/staff collapses multiple Authentik accounts with the same email into one
row (username `joseph` + `joseph@...` → one), flags dup_accounts for cleanup.
- provision LINKS an existing Employee (match user_id → company_email → name)
instead of creating a duplicate — fixes the Aurélie HR-EMP-107 doublon risk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings → Staff tab: one row per internal Authentik user with provisioning
state (active, groups, System User / Employee / Dispatch Technician / identity),
flagging orphan (active but no System User → not provisioned, e.g. Karim
Takougang), departed (inactive/identity-departed), ok.
Hub /auth/staff (GET reconciliation) + /auth/staff/provision (groups + System
User + Employee + Tech + identity in one click) + /auth/staff/active
(deactivate/reactivate, non-destructive) + /auth/staff/impact + guarded DELETE
(refuses with 409 if tickets/jobs reference the person → offers deactivate).
Writes admin-gated. identity.js gains programmatic upsert/setActive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One person = one identity { label, primary_email (SSO login), alias_emails[],
tech_id, active, kind }. Every email + tech ID resolves to it.
Hub: lib/identity.js (NEW, SOURCE UNIQUE) — resolver + secured endpoints
(/identity/map|resolve read; upsert/alias/merge/delete admin-only). server.js
mounts /identity. auth.getDisplayNameByEmail consults the identity label first
(any alias → full name everywhere). Seed: Louis-Paul Bourdon, Louis Morneau.
SPA: composables/useIdentity + api/identity. IssueDetail dedupes assignment by
identity (m'ajouter + search = same person → no double chip), shows full names,
and hides departed people (active:false) from the picker. IdentityManager.vue
(Settings → Identités tab): edit label/aliases/tech link, merge duplicates,
block/reactivate (departed = non-destructive: hidden from pickers, history kept).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the jobs-to-assign pool unification whose PlanificationPage wiring
landed in 9c49054 (which referenced these files before they were committed):
- JobPool.vue: one shared component (floating/docked/mobile) replacing the 3
divergent pool implementations
- useJobPool.js: shared filter/sort/group/badge composable
- RouteMap.vue: live tech icons join the explode/fan; single-tech real Traccar
day-route layer
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add "Bloquer / job générique…" to the timeline cell menu (opens on empty-cell
click or right-click). Opens a small dialog prefilled with the tech + day:
editable title, full-day toggle (= that cell's shift length) or custom
start/duration, standard priority (default Moyenne), type, and a "generate a
linked ticket" toggle.
New hub POST /roster/filler-job creates a standard-priority assigned Dispatch
Job (+ optional ERPNext Issue via source_issue, best-effort so a ticket failure
never blocks the job). Since timed jobs now count toward occupancy, this removes
the tech from regular dispatch — unlike /reserve which stays soft for urgencies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the per-tech skill editor (chips ordered by priority + per-skill Score
★ + Cadence %) into shared components/planif/SkillCadenceTable.vue, used by BOTH
the click-a-tech popover AND the team tool — one template everywhere.
Rebuild "Équipe — cadence & coût" into "Équipe — compétences, cadence & coût":
each tech is an expandable row showing SkillCadenceTable (per-skill cadence) +
global cadence (default 100%) + cost. Operates on live tech objects so edits
persist via the existing handlers. Folds "Gérer les compétences" (tag catalog:
rename/recolor/delete) in as a collapsible section, and collapses the two menu
entries into one "Équipe — compétences · cadence · coût".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three fixes from live feedback:
1. techOccupancy counted only jobs WITH a start_time → legacy osTicket jobs
(assigned, no appointment hour) showed as free (e.g. Houssam "40h libre"
with 6 jobs). Now untimed assigned jobs count toward busy (rendered as
hatched "unscheduled" blocks) and jobs match by technician_id OR docname.
techOccupancy also accepts CSV skills (require ALL) for multi-skill needs.
2. Support now maps to a 'support' skill (was []), so it lists only
support-skilled agents — techs with no skills set are excluded. Added
'support' to SKILL_VOCAB + DEPARTMENT_SKILLS.
3. AvailabilityByReason skills list is editable: auto-added skills (e.g.
'sans-fil' from wireless service address) can be removed per-chip when
not relevant. Bands + summary now reflect ALL selected skills.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Absences (congé/pause) passent par le modèle local -> Publier (journal,
undo, dirty badge) au lieu d'écrire directement au serveur. Applique à la
grille ET au calendrier par tech (TechScheduleDialog).
- Fix capacité "48h" : l'estimation ignorait l'absence et le patron hebdo.
- Menu de cellule (Jour/Soir/Garde/Absent/heures) extrait en composant
partagé PlanifCellMenu, réutilisé par la grille et le calendrier par tech.
- Raccourci "Jour" = 8-16 (pas 8-17) ; saisie rapide accepte "816".
- Nouvelle vue Mois (MonthOverview) : qui est en quart/absent par jour +
couverture par compétence (heures requises vs heures en quart). Remplace
l'ancien module "Demande - effectif requis par créneau".
Refonte du modèle d'interaction (retour user) + correction du bug d'affichage des assistants.
Bug corrigé (racine = hub) : getJobTeam lisait le doctype-enfant « Dispatch Job Assistant »
via erp.list → PermissionError pour le compte de service → renvoyait [] en silence. Résultat :
chips assistants jamais affichées, tech non exclu de l'autosuggest, et POST add/remove partait
de [] → écrasait les assistants existants. Fix : lire `assistants` VIA LE DOC PARENT (child table
embarquée, sans vérif de perm sur l'enfant). Vérifié : getJobTeam(LEG-253958) renvoie l'assistant.
Nouveau modèle (AssignmentField) :
- UN champ « Ajouter un participant » (techs + compétences en tête, puis tous les utilisateurs).
1er ajouté = assigné/lead ; suivants = assistant par défaut.
- Échelle d'implication par personne (menu sur la chip) : Suiveur ‹ Assistant ‹ Sur place (équipe).
· Suiveur (#) = reçoit les MàJ, hors équipe (/conversations/follow).
· Assistant (CC) = sur l'équipe, SANS bloc horaire (Dispatch Job Assistant pinned=0).
· Sur place = assistant AVEC bloc réservé dans son horaire (pinned=1) — masqué sur ticket (can-onsite=false).
- « Assist » / « Suivre » deviennent des BASCULES sur l'utilisateur connecté (moi), plus des liens d'expansion.
- Suivi keyé par courriel si connu, sinon tech_id (repli).
Câblage : PlanificationPage jdSetAssistant({value,label,onsite}) → addAssistant pinned=onsite?1:0
(le hub `add` remplace la ligne du même tech → sert aussi à changer de niveau). IssueDetail : can-onsite=false.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fusionne l'assignation en UN autosuggest + chips à 3 rôles, réutilisé sur le
volet tâche (PlanificationPage) ET le détail ticket (IssueDetail).
- Nouveau composant partagé AssignmentField.vue : chips distinctes À (assigné /
lead) · CC (assistant / renfort) · # (follower). Autosuggest par défaut =
techniciens ; 1er choisi = assigné, 2e+ = assistant. Mini-liens « Assist » et
« #follow » étendent la liste à TOUS les utilisateurs (assistant ou follower
depuis n'importe qui). Le suivi (follower) est géré dans le composant via
/conversations/follow (identique sur toutes les surfaces).
- TechSelect : props optionnelles searchFn (recherche async « tous les
utilisateurs ») + noOptionLabel — amélioration du composant partagé, pas de fork.
- PlanificationPage : le champ To/Cc devient AssignmentField (assigné→jdAssignTech,
assistant→addAssistant, désassigner→jdUnassign) ; adaptateurs jdAssignee /
jdAssistants / jdFieldAssist.
- IssueDetail : remplace le multiselect _assign par AssignmentField (1er _assign =
À, suivants = CC ; frappe assign_to) ; pool par défaut = utilisateurs assignables.
- Hub (conversation.js) : POST /conversations/follow accepte un courriel cible
(abonner n'importe quel utilisateur) ; nouveau GET /conversations/followers
?doctype&name → liste des abonnés (pastilles #).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- capacité/occupation par jour : retranche les (tech,jour) en congé approuvé /
pause / archivé du dénominateur (dashboard cap_h + bande Planif visStat) —
fini le « 24/150 » gonflé par des techs indisponibles
- archive technicien : store hub durable (data/archived_techs.json) au lieu
d'écrire status=Archivé (ERPNext v16 rejette la valeur hors liste → 500) ;
exclusion partout (roster/solveur/créneaux/occupation) + restauration
- TechScheduleDialog calendrier : jours SANS quart grisés (comme week-end),
jours AVEC quart en blanc + mince barre d'occupation → vraie dispo du tech
- techOccupancy : horizon jusqu'à 42 j (calendrier mois)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
- 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>
- 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>
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>
#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>
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>