Commit Graph

124 Commits

Author SHA1 Message Date
louispaulb
77e4411ba2 feat(geofence): réconciliation véhicule — assistant / mauvaise affectation (suggestions)
Niveau 2 de Request 1 : à partir des traces GPS, détecte QUELS véhicules ont
séjourné sur un job, puis SUGGÈRE (jamais auto) :
- ASSISTANT : le véhicule du tech assigné était là ET un autre a séjourné → propose
  d'ajouter le tech de cet autre véhicule comme assistant.
- MAUVAISE AFFECTATION : le véhicule du tech assigné n'a PAS été vu mais un autre a
  fait la visite → le tech a pris un autre camion (mapping non mis à jour).
Garde-fous : séjour (pas passage), fenêtre du jour, garde MÊME-ADRESSE (un véhicule
présent pour SON propre job co-localisé n'est pas un assistant). Cache traces/jour + 1 h/job.

geofence.js : onsiteReview(job) + dismissOnsite ; roster.js : GET /roster/job/:name/onsite
+ POST .../onsite-dismiss ; api/roster : jobOnsite/onsiteDismiss ; GeofenceTimeline : bloc
suggestions avec [Ajouter comme assistant] (addAssistant) / [Assigner ce véhicule au tech]
(setTechTraccarDevice, si device non mappé) / [Ignorer] (dismiss) — confirmées, non destructives.
Mauvaise-affectation vers le véhicule d'UN AUTRE tech = ambigu → pas d'auto-bouton (flag + Ignorer).

Vérifié live : 45 devices ; LEG-255021 → « 37 - Anthony » = tech assigné, 0 suggestion (OK) ;
scan du jour : 31 jobs, 11 avec véhicule sur place, 3 mauvaises affectations détectées.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 13:28:40 -04:00
louispaulb
f793eb4257 feat(service): bouton « Do Stuff » sur le service → ouvre la vue appareil
Demande user : une façon SIMPLE d'accéder au Do Stuff. Ajout d'un bouton « Do Stuff —
appareil » sur le détail du service (SubscriptionDetail), comme le bouton Do Stuff de F.
Résout l'appareil du service (Service Equipment par lieu de service, sinon par client ;
préfère un appareil actif avec n° de série) et navigate('Service Equipment', name) →
la fiche ouvre EquipmentDetail = la vue « Do Stuff » (signal Rx/Tx, débit WAN en direct,
actions device_action). Marche pour fibre ET sans-fil (l'appareil résolu s'ouvre ; les
stats airMAX sans-fil s'y afficheront quand leur lecteur sera branché).

Build spa OK, md5 match. Rendu live = fiche sous SSO (prod).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 13:14:10 -04:00
louispaulb
877dfa5c44 feat(geofence): fige le véhicule Traccar sur chaque événement de passage
Demande user : garder l'historique du véhicule au moment du hit géofence — rester exact
même si le tech change de véhicule un autre jour. Les événements (en_route/on_site/departed)
enregistrent désormais deviceId+deviceName ; rec.device = véhicule du passage. runScan
(live) fige le device du fix courant ; reconcileFromTrack (rejeu trace) fige le device
rejoué ce jour-là. timeline() renvoie device ; GeofenceTimeline affiche « Véhicule : … ».

Vérifié : hub OK, timeline() expose device (null sur les 351 events antérieurs = normal ;
capturé pour les prochains passages). Build spa OK, md5 match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:57:50 -04:00
louispaulb
3547f4a680 feat(map): véhicules Traccar libellés par initiales du TECH, repli sur le device
Demande user : afficher les véhicules Traccar avec les initiales du TECHNICIEN assigné,
et ne retomber sur les initiales du device Traccar QUE si aucun tech n'est assigné au véhicule.

- hub /roster/tech-positions : part désormais de TOUS les devices Traccar (pas seulement ceux
  liés à un tech). Pour chaque device localisé : techName si un tech y est assigné (via
  traccar_device_id), sinon deviceName ; renvoie techName + deviceName + name résolu (tech>device).
  Exclut l'île nulle (0,0 = jamais localisé). Live : 23 positions (14 tech, 9 device-only).
- RouteMap : libellé marqueur = initials(techName || deviceName || name) (tech prioritaire).
- PlanificationPage dayLivePositions : les véhicules SANS tech s'affichent aussi (gris),
  filtrés par visibilité seulement s'ils ont un tech.

PlanificationPage.vue = co-édité → DÉPLOYÉ (build) mais NON commité (hunk à isoler).
Vérifié : endpoint live OK ; build spa OK, md5 match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:44:04 -04:00
louispaulb
08a68016ec feat(job-map): itinéraire routier OSRM tracé DANS la carte MapLibre
Le module carte du job (JobMapModule, réutilisé volet Tournées + détail ticket) avait
un repère + tracé GPS Traccar mais AUCUN itinéraire in-app (seulement un lien Google
externe). Ajout d'un toggle « Itinéraire routier » : trace le trajet routier RÉEL via
l'OSRM auto-hébergé (roster.osrmRoute → geometry) sur la carte MapLibre + affiche km/min,
et cadre la vue sur le trajet. Départ = position live du tech assigné (traccarLive),
sinon géoloc du navigateur ; destination = coords du SERVICE (GPS de l'adresse de service,
désormais fiables). Lien « Navigation » Google conservé pour la voix sur téléphone.

Réutilise l'infra existante (OSRM /roster/osrm-route, MapLibre/OSM auto-hébergé, ZÉRO
Mapbox). Vérifié : build spa OK, 0 erreur HMR. Rendu live (fiche/ticket) = SSO côté prod.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:25:52 -04:00
louispaulb
69290d7b07 feat(assistant): désambiguïsation client via options cliquables (un seul champ)
Remplace le 2e champ « rechercher un client » du chat par une liste d'options
que l'ASSISTANT propose DANS le fil (comme un menu de désambiguïsation). Plus
naturel, plus simple : on écrit une seule chose, et si le client est ambigu
l'assistant liste les candidats probables — un clic les envoie comme tour suivant.

Backend (staff-agent.js + agent-tools.json) :
- lecteur find_customer(query) → CANDIDATS via /collab/customer-search (réutilise
  l'endpoint de l'ancien typeahead). 1 candidat → l'assistant prend l'id ; >1 →
  il demande « lequel ? » sans réénumérer (l'UI affiche la liste).
- execToolPlan capte les candidats (find_customer + check_service ambigu) dans
  ctx.options ; plan() renvoie { options } ; prompt : règle « identifier le client ».
Frontend (OrchestratorDialog.vue + MainLayout.vue) :
- retire le q-select entitySearch (+ la prop et le binding MainLayout) ; composer
  = un seul champ. Rend d.options en q-list cliquable ; pickOption envoie
  « label (id) » comme tour suivant ; options effacées à l'envoi/au reset.

Vérifié live (dev→hub prod) : « redémarre le modem du client Tremblay » → 1 champ,
8 options cliquables + « lequel ? » ; clic « Tremblay (Filtr-Aqua) Sylvain » →
tour « … (C-SYLVT…) » envoyé, options effacées, l'assistant continue ; 0 erreur console.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 08:54:53 -04:00
louispaulb
19e2c1fea0 feat(assistant): chat moderne — saisie en bas, fil qui défile en bulles
- OrchestratorDialog restructuré en vraie fenêtre de chat : carte à hauteur
  fixe (76vh), en-tête + conversation qui défile + composer épinglé en bas.
  On écrit SOUS la discussion (comme tout chat), l'input se vide à l'envoi,
  auto-défilement vers le bas à chaque nouveau message. Confirmation du plan
  déplacée en ligne dans le fil (plus de pied de page). État initial « Comment
  puis-je aider ? » + exemples. Recherche client/adresse intégrée au composer.
- MonAujourdhui : « Allouer un technicien » → « Assistant » (icône auto_awesome),
  ouvre le chat vierge ; visible aussi pour les rôles Boîte de réception.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 21:33:53 -04:00
louispaulb
0372176fc8 feat(assistant): vrai CHAT multi-tours — on peut répondre aux questions de l'assistant
Avant : l'assistant posait une question (« Souhaitez-vous que je crée… ? ») mais le dialogue était mono-coup — impossible
de répondre (pas de fil, chaque envoi repartait de zéro). Maintenant c'est une conversation :
- Backend staff-agent.plan(text, email, history) : accepte les tours précédents (user/assistant, 10 derniers) → contexte ;
  POST /staff-agent {text, history}.
- OrchestratorDialog : fil de bulles (user/assistant), le champ se vide après envoi pour RÉPONDRE, historique renvoyé à
  chaque tour, bouton « Envoyer » en mode conversation. Le plan (actions à confirmer) suit le dernier tour.
Vérifié : (backend) « vérifier signal C-LPB4 » → diagnostic (réparation, 12 techs/431h) ; « oui, crée » AVEC history →
propose create_job(C-LPB4, Réparation). (UI) 4 bulles [user,assistant,user,assistant], champ vidé, action « Créer un job »
affichée, 0 erreur console. Déployé.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:07:55 -04:00
louispaulb
c4c4cc6575 feat(ops): « Allouer un technicien » = chat IA (Gemini) avec typeahead client/adresse
L'action ouvrait déjà le copilote Gemini (staff-agent) mais sans autocomplétion → il fallait taper le client exactement.
Ajout d'un typeahead d'ENTITÉ dans le chat (OrchestratorDialog, prop entity-search) : on tape → suggestions live
(nom · adresse · téléphone · courriel via /collab/customer-search) → le choix INJECTE l'entité RÉSOLUE « client <id>
(<nom> — <adresse>) » dans la demande, que Gemini extrait pour agir (proposer_rdv_client / assign_tech). Activé sur le
copilote STAFF (MainLayout entity-search) ; graine « Allouer un technicien : » (MonAujourdhui). Séparateur propre après « : ».
Vérifié en dev : « tremblay » → 8 suggestions → choix → prompt « Allouer un technicien : client C-SYLVT… (Tremblay…) » ;
endpoint /collab/customer-search OK (12 matches) ; 0 erreur console. Build OK, leak-clean, déployé.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:30:48 -04:00
louispaulb
788d969090 chore(reconcile): commit MES hunks déployés dans les fichiers co-édités (split via git apply --cached)
Réconciliation de l'audit : isole et commite UNIQUEMENT mon travail déployé-non-commité dans 7 fichiers co-édités,
sans toucher au working tree (donc sans capturer le travail en cours des autres sessions, qui reste non commité) :
- server.js : SSE event 'ready' + retry:3000 ; routes /olt/onu/plan|run + /olt/wifi-clients.
- conversation.js : endpoint /conversations/my-inbox-counts (accueil : en attente / suivis / en retard).
- useConversations.js : armSSE (reconnexion+resync+repli poll 25s) + cleanup.
- PlanificationPage.vue : chips géofence (Lane 1c) + sélecteur « Générer l'horaire » (Move 3) + isLate (arrivée en retard).
- SettingsPage.vue : section « Thème & couleurs » (ThemeEditor).
- IssueDetail.vue + ConversationPanel.vue : props source-issue/customer/service-location/phone (« Proposer au client »).
EXCLUS (restent non commités, propriété d'autres sessions) : /service-location, /conversations/msg-visibility, retrait
champ Mapbox, fix conv-message 'belongs', hiérarchie parent_incident, JobMediaModule, etc. Vérifié : 0 marqueur étranger
dans le staged. Tout est déjà LIVE ; ceci n'aligne que le dépôt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:06:47 -04:00
louispaulb
60f6d71615 chore(ops): commit ThemeEditor.vue (extrait de l'ex-page /theme, consolidé dans Paramètres)
Composant autonome créé lors de la consolidation Thème→Paramètres (redirect routeur /theme→/settings déjà commité en
23f0f72). Fichier 100% mien, sans mélange → seul élément sûr à commiter de l'audit ; le point de montage (import dans
SettingsPage.vue) reste non commité car ce fichier est co-édité (voir rapport d'audit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:00:39 -04:00
louispaulb
4718d025f3 improve(ops): accueil = tableau VIVANT — rafraîchit les compteurs (60 s + retour d'onglet)
L'accueil ne chargeait qu'au montage. Ajout : refetch toutes les 60 s + immédiat au retour sur l'onglet
(visibilitychange), avec nettoyage à onUnmounted. dash() conserve la valeur précédente pendant le refetch → pas de
clignotement vers « … ». Les compteurs « en attente de réponse / en retard / interventions » restent à jour sans
rechargement. Build OK, leak-clean, déployé.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:56:16 -04:00
louispaulb
f8af4235fb improve(ops): accueil — détecte les ARRIVÉES EN RETARD sur les interventions du jour
Enrichit la carte « Interventions aujourd'hui » (au-delà du simple découpage par statut) : croise l'heure de début
prévue avec l'état géofence live → pastille ROUGE « N en retard » quand le début est passé de > 20 min mais le tech
n'est PAS montré sur place (ni parti). Signal proactif de retard/no-show pour le répartiteur. Réutilise /roster/geofence-states.
Propre (composant seul, aucun changement hub). Vérifié en dev : rendu 3 cartes, aucune erreur console (0 job aujourd'hui
en dev → pastille masquée, correct). Build OK, leak-clean, déployé.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:34:28 -04:00
louispaulb
0d80c323f0 improve(ops): accueil — chiffres HONNÊTES depuis les vraies sources (pas de proxy trompeur)
Le décompte « exact » a révélé que la métrique elle-même était faible : _assign ERPNext quasi vide ici (1726 « open »
tous non assignés). Corrigé pour refléter la RÉALITÉ, via /conversations/my-inbox-counts :
- Boîte : « en attente de réponse » = conversations actives dont le DERNIER message vient du client (212) + contexte
  « N actives » — vraiment actionnable, vs « 540 total » ou « 1726 tickets ouverts ».
- Tickets sur moi : basé sur le SUIVI (le vrai « Mes tickets » d'OPS), pas _assign ; + « en retard » (ouvert > 48 h) en rouge.
Endpoint hub my-inbox-counts (conversation.js, déployé — co-édité, non commité ici). Vérifié en dev : 212 en attente / 540
actives / 0 suivis, 0 erreur console. Build OK, leak-clean, déployé.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:04:57 -04:00
louispaulb
e7b94947cf feat(dispatch): « Proposer au client » directement dans le flux Dispo par motif (in-ticket/conversation)
Step 1/3 des améliorations. AvailabilityByReason (dialogue partagé, monté dans le ticket ET la conversation) n'offrait
que « Trouver un créneau » (assigner soi-même). Ajout du 2e chemin — laisser le CLIENT choisir — là où le diagnostic se fait :
- Bouton « Proposer au client » → api proposeAppointment → hub /roster/book/propose : résout OU **crée** un Dispatch Job
  ouvert (via createDispatchJob) puis envoie le lien /book par texto. Toast avec « Copier le lien » si pas de SMS.
- roster.js : /roster/book/propose crée le job ouvert si aucun (client ou sujet requis) + pose required_skill.
- Props de contexte passées par les 2 panneaux (source_issue/customer/service_location/phone) — édition additive des
  panneaux co-édités = NON commitée ici (déployée via le bundle).
Vérifié : endpoint 404 attendu sans contexte ; « Proposer au client » compilé dans le bundle ; build OK, leak-clean, déployé.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:25:37 -04:00
louispaulb
dbbcd721e4 feat(ops): accueil « Mon aujourd'hui » v2 — chiffres exacts + état terrain live + actions réelles
Applique « ne pas réutiliser une UX/métrique faible telle quelle » :
- Boîte de réception : remplace le compteur de session 'unread' (démarrait à 0, trompeur) par un VRAI décompte via
  /conversations/inbox-tickets — « à trier » (non assignés) + total ouvert. Tickets sur moi = _assign contient mon courriel.
- Interventions aujourd'hui : ajoute l'ÉTAT TERRAIN LIVE (en route / sur place via /roster/geofence-states, pastilles
  qui pulsent) en plus du découpage par statut.
- Barre d'ACTIONS réelles : « Allouer un technicien » → ouvre le copilote NL PRÉ-REMPLI (event ops:assistant → MainLayout),
  « Nouvelle vente », « Rechercher ⌘K ». Le pont ops:assistant est ajouté dans MainLayout (propre).
- États chargement (« … ») / vide, focus clavier, prefers-reduced-motion.
Vérifié en dev : rendu, 199 à trier (donnée réelle), event allocate + palette OK, 0 erreur console. Build OK, leak-clean, déployé.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:10:07 -04:00
louispaulb
386e36f863 feat(ops): accueil « Mon aujourd'hui » adapté au rôle en tête du tableau de bord
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>
2026-07-19 14:58:33 -04:00
louispaulb
89f7efc4ba fix(realtime): l'app terrain et le panneau pannes SURVIVENT aux redéploiements du hub
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>
2026-07-19 12:26:37 -04:00
louispaulb
daeb86008b chore(ops): supprime le sous-système dispatch MORT (23 fichiers, 0 importeur)
Lane 4 (nettoyage) — vérifié 0 importeur vivant (aucun barrel, aucun import.meta.glob ; build OK après suppression) :
- stores/dispatch.js + 9 composables (useGpsTracking/useAutoDispatch/useBottomPanel/useJobOffers/useDragDrop/useScheduler/
  useSelection/useUndo/useResourceFilter)
- features/workforce/ (api/assignment + useAssignment + useResources)
- components/dispatch/NlpInput.vue
- 11/12 modules/dispatch/components/* (BottomPanel/CreateOfferModal/JobEditModal/MonthCalendar/OfferPoolPanel/
  PublishScheduleModal/RightPanel/SbContextMenu/SbModal/TimelineRow/WeekCalendar)
GARDÉ : modules/dispatch/components/SuggestSlotsDialog.vue (vivant — ClientDetailPage + PlanificationPage), OccupancyBands,
AvailabilityByReason, api/dispatch. Reliquat Mapbox mort (JobEditModal static image) supprimé avec le fichier.
Bundle inchangé (code déjà tree-shaké car 0 importeur) → pas de redéploiement requis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:32:18 -04:00
louispaulb
da9c918fa9 feat(ux): gate ONU device actions + delete behind manage_settings, own block
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>
2026-07-19 10:30:45 -04:00
louispaulb
5dd38eb79b feat(gps): vue « appareil GPS → technicien » (assignation inverse) + non-associés
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>
2026-07-19 08:42:32 -04:00
louispaulb
23dde4ecf0 feat(ops): native Payment Method/Arrangement + VoIP Line sections + G3 Raisecom SNMP writes
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>
2026-07-18 22:39:59 -04:00
louispaulb
3d4700decf feat(ops): native Dispatch Job/Communication detail + ONU write parity (Phase 1-2)
Phase 1 (détail natif partout, fini le desk ERPNext) :
- DispatchJobDetail.vue : vue NATIVE du Dispatch Job (assemble les modules partagés : carte/tracé, géofence, photos terrain, durée, compétences, équipe+assign 409-aware, fil+réponse) → SECTION_MAP['Dispatch Job']. Répond à la plainte « le job tombe sur la grille générique + bouton desk ».
- CommunicationDetail.vue : vue NATIVE d'une Communication (courriel/appel) → SECTION_MAP['Communication'] ; HTML assaini.
- 3 pages orphelines (Réseau/GPON, Téléphonie SIP, Flux agent IA) surfacées en nav gatée view_settings ; OCR rattaché à Factures fournisseurs (bouton Numériser) ; stubs « Bientôt » retirés de Rapports.

Phase 2 (parité écriture fibre — remplace le « Do Stuff » de F) :
- lib/olt-ops.js : plan(lecture seule : résout OLT/slot/port + payload n8n exact + effets + avertissements) / run(fire le verbe n8n dostuff PUIS miroir Service Equipment). Idempotent (clé, fenêtre 10 min), confirm obligatoire, tech-3 seulement (tech-2 Raisecom → SNMP natif = G3 à venir). + wifi-clients (G1).
- OnuActionsPanel.vue : suspend/rétablir/forfait/remplacer/retirer/activer en plan→confirm→run, monté dans EquipmentDetail.

Backend déployé+vérifié live (plan/run/guards/auth : plan 200 résout EQP réel, run sans confirm→refus, sans auth→401). SPA déployée /opt/ops-app. AUCUNE écriture réseau réelle tirée (run = à déclencher par un humain via le bouton).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:04:19 -04:00
louispaulb
cd25fa2504 fix(dispatch): double-assign F + médias terrain dans le ticket + deep links natifs
- 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>
2026-07-18 18:02:37 -04:00
louispaulb
ef7b51399d refactor(planif): « Bloquer du temps » = UN job/ticket qui occupe la timeline, priorité = curseur dur/souple
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>
2026-07-16 18:38:17 -04:00
louispaulb
ba6967adc6 refactor(planif): fusion « Réserver du temps » + « Bloquer / job générique » en UN dialogue
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>
2026-07-16 14:52:12 -04:00
louispaulb
e8fd69c6f8 feat(planif): pause indéfinie STAGÉE → visible tout de suite, commise au Publier (retire quarts futurs + relève jobs)
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>
2026-07-16 13:33:08 -04:00
louispaulb
48edede793 refactor(planif): désencombrer l'en-tête selon UX_REFACTORING_PROMPT (PL1/PL2/PL4/PL5/PL6)
- PL5 : sous-menu « Modèles de semaine » (menu-dans-menu) → dialogue dédié
  components/planif/WeekTemplatesDialog.vue (props données / emits save·apply·
  set-default·delete ; état + mutations restent dans la page, EmptyState réutilisé)
- PL2 : menu Outils 13 items + sous-menu → 11 items à plat ; retrait des 2 entrées
  redondantes avec la barre (« À assigner », vue Mois)
- PL1/PL4 : barre allégée — bouton ★ modèle par défaut déplacé dans le ⋮ (groupé)
  + ⌘K (planif-templates, planif-apply-default) ; HelpHint « Suggérer » retiré
  (l'explication vit déjà dans le bandeau du dialogue)
- PL6 : champ orphelin « Max h/sem » supprimé (ref jamais consommée = code mort)
- docs : statuts PL1/PL2/PL4/PL5/PL6 mis à jour dans UX_REFACTORING_PROMPT.md

Vérifié : build SPA OK · aperçu 9001 — Outils/⋮/dialogue exercés, chaîne emit
save → prompt parent OK, grille Semaine + Tournées intactes, 0 nouvelle erreur
console (les « Anchor: target null » préexistent au baseline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:56:34 -04:00
louispaulb
718698f0d0 feat(ops): gate ticket activation on pending subs + show consolidated proration preview
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>
2026-07-10 16:24:27 -04:00
louispaulb
e1d3212bf7 feat(ops): ticket Statut control (Complété/Annulé) + sale activation → draft invoice
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>
2026-07-10 15:04:08 -04:00
louispaulb
e8dc86e058 feat(ops): one-click identity linking (Authentik ↔ Employee ↔ Tech), merge-safe
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>
2026-07-10 14:27:30 -04:00
louispaulb
d0f899cf6d feat(ops): staff console — surface dup accounts + per-user schedule deep-link
- 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>
2026-07-10 13:44:56 -04:00
louispaulb
11c53c956a feat(ops): staff console — one-click dedupe of duplicate Authentik accounts
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>
2026-07-10 13:33:22 -04:00
louispaulb
148cb9a320 fix(ops): staff recon — alias-aware employee, dedupe Authentik accounts, link-not-duplicate
- 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>
2026-07-10 13:20:09 -04:00
louispaulb
b43f092da3 feat(ops): staff reconciliation console (Authentik ↔ System User/Employee/Tech)
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>
2026-07-10 13:01:33 -04:00
louispaulb
dec8321823 feat(ops): unified staff identity (label + email aliases + tech link + departed flag)
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>
2026-07-10 12:22:48 -04:00
louispaulb
14530787bb consolidate uncommitted multi-session work (NL staff-agent, user avatars/profil, HelpHint, ticket detail modules, skill icons, hub agent/payments/sync)
Lot de travaux accumulés non commités (plusieurs sessions), vérifiés : hub `node --check`
tout OK + build SPA propre.
- ops : AssignmentField, CommandPalette, IssueDetail/DetailModal + detail-sections/modules,
  UserAvatar/useAvatar, UserProfileDialog/EmployeeEditDialog, HelpHint, useSkillIcons,
  pages (Dashboard/Equipe/Clients/Tickets/Reports/Settings/Evaluations/LegacySync…),
  usePermissions/useUserGroups/useDetailModal, MainLayout, TicketStatusControl
- hub : staff-agent.js (couche commandes NL), agent.js/agent-tools.json/voice-agent.js,
  avatars.js, conversation.js, payments.js, sync-orchestrator.js, auth.js, campaigns.js,
  legacy-dispatch-sync.js, server.js

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:43:50 -04:00
louispaulb
cfcd7c04cc feat(ops/planif): add JobPool + useJobPool + RouteMap route/explode
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>
2026-07-09 20:30:36 -04:00
louispaulb
9c49054b8d feat(ops): quick filler job from cell menu (blocks tech from dispatch)
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>
2026-07-09 19:58:18 -04:00
louispaulb
9145624949 feat(ops): unify tech skill/cadence editor + merge team tools
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>
2026-07-09 14:57:01 -04:00
louispaulb
7df6ad30e3 fix(ops): accurate occupancy denominator + editable skills + support filter
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>
2026-07-09 14:28:42 -04:00
louispaulb
3489576212 Planif: congés publish-required, menu de cellule partagé, vue Mois
- 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".
2026-07-09 11:32:54 -04:00
louispaulb
893ac8cd4a Déclutter: primitives réutilisables + palette ⌘K + Planif épurée
Ajoute 3 primitives partagées pour standardiser le désencombrement
(philosophie 3 paliers : coup d'œil → survol → détail) :
- OverflowMenu.vue : « ⋮ » standardisé (une action primaire visible,
  le secondaire dans le menu).
- DisclosureSection.vue : section repliée par défaut (résumé + corps
  révélé), état persisté par utilisateur (useUserPrefs).
- CommandPalette.vue + useCommandPalette.js : palette globale ⌘K
  (actions rapides + pages + actions contextuelles de page + recherche
  clients/équipe), registre d'actions par page = escape hatch.

Câblage MainLayout : palette montée + raccourci Cmd/Ctrl+K + indice ⌘K
dans la recherche desktop.

Adoptions :
- RapportsPage : PageHeader + stubs « Opérations » repliés + rapports
  enregistrés dans la palette.
- DashboardPage : bloc admin replié (DisclosureSection) + PPA rétrogradé
  dans un OverflowMenu (anti clic accidentel) + actions dans la palette.
- PlanificationPage : le panneau « Jobs à assigner » n'ouvre plus au
  chargement ; alerte « N à assigner » par jour (clic → panneau filtré) ;
  undo/redo/refresh regroupés dans un OverflowMenu ; actions dans la palette.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 06:53:24 -04:00
louispaulb
f4a6d07a8b feat(ops): reason → skill → skill-aware availability module
Bridge reason → required skill → availability so the occupancy denominator
reflects the skill a job needs ("70 h installation" vs "150 h all techs").

Hub:
- lib/skill-resolver.js (NEW, SOURCE UNIQUE): shared techHasSkill(s) predicate,
  deptToSkill (moved here, re-exported from roster.js — no cycle),
  DEPARTMENT_SKILLS bridge, resolveSkills({text,department,jobType,useAI})
  (chip → rule → Gemini fallback, reusing classifyEmail pattern).
- roster.js: capacityByDay(start,days,skills) filters denominator to qualified
  techs (+ guards used/due). New POST /roster/resolve-skills, /roster/capacity?skill=.
  Backward-compatible (no skill = prior behavior).
- dispatch.js: techOccupancy/suggestSlots reuse the shared predicate.

SPA:
- config/departments.js: skills[]+dur per department + skillsForDepartment();
  added Réparation reason.
- components/shared/OccupancyBands.vue (NEW): vertical-strip grid extracted from
  SuggestSlotsDialog (now reuses it).
- components/shared/AvailabilityByReason.vue (NEW): reason chips + dictate→AI →
  resolved skill → skill-filtered bands + adjusted-denominator summary.
  Mounted in ClientDetailPage, IssueDetail, ConversationPanel.
- api/roster.js: getCapacity(start,days,skill) + resolveSkills().
- Planif + Dashboard capacity bands pass skill to /roster/capacity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:33:30 -04:00
louispaulb
6bf323b18e fix(ops): champ d'assignation — échelle d'implication + bascules « moi » + fix lecture équipe
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>
2026-07-08 17:40:36 -04:00
louispaulb
2f81cff892 feat(ops): champ d'assignation MULTIFONCTION (À / CC / #) partagé
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>
2026-07-08 16:49:57 -04:00
louispaulb
844eff9a49 fix: capacité réelle (hors congé/pause/archivé), archive via store hub, calendrier horaire tech
- 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>
2026-07-08 14:46:21 -04:00
louispaulb
d7412fcced #27 Standardise ticket-detail rendering (status / tags / row component)
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>
2026-07-08 14:31:39 -04:00
louispaulb
6224271e07 ticket : afficher le technicien assigné EN TÊTE du panneau détail (IssueDetail)
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>
2026-07-08 13:53:21 -04:00
louispaulb
13039be301 feat: suivi généralisé, occupation ressources, horaire tech, revert journal
Follow / subscribe (au-delà des tickets)
- hub: store généralisé /conversations/follow {doctype,name} (follows.json,
  migre ticket_follows.json sous Issue) ; ticket-follow = alias Issue
- InterveneDialog: tuile « Suivre » câblée (toggle + état) ; TicketsPage sync

« Trouver un créneau »
- hub: /dispatch/occupancy (techOccupancy) — occupation par tech filtrée skill
- SuggestSlotsDialog: panneau occupation (bandes verticales) + props de
  pré-remplissage (adresse/coords/skill)
- ClientDetailPage: entrée directe « Trouver un créneau » (appel/réparation)
- fix moteur créneaux: suggestSlots + techOccupancy respectent désormais les
  congés (Tech Availability approuvé), « En pause » et « Archivé »

Horaire par technicien (components/planif/TechScheduleDialog)
- écran unique: pause indéfinie + motif · horaire récurrent (réutilise
  WeeklyScheduleEditor) · calendrier du mois (cases carrées, glisser-
  sélectionner + pinceau congé/maladie/indispo/effacer) · archivage réversible
- hub: /roster/technician/:id/archive (status Archivé) + exclusion au fetch
  (_fetchTechniciansRaw) + /roster/technicians?archived=1 (restauration)
- icône « event » de la rangée tech remplace le bouton pause

Journal des changements (icône history)
- chaque changement de quart révertible individuellement (bouton « annuler »),
  à la façon de la liste « Publier »

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 12:40:22 -04:00