From c05a6272b7e882d0c126961eb0a386cab1c1c265 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sun, 5 Jul 2026 17:09:22 -0400 Subject: [PATCH] feat(ops): pickups = ~5min jobs + generic visit icon (handyman, not bolt) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/ops/src/pages/PlanificationPage.vue | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index cb48051..ad3841b 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -2093,7 +2093,7 @@ async function fetchKbMatrix (techId) { } let _kbMatT = null // le watch déclencheur est enregistré PLUS BAS (après la déclaration de occByTechDay) pour éviter la zone morte temporelle const kbColor = (j) => j.skill ? getTagColor(j.skill) : (j.required_skill ? getTagColor(j.required_skill) : (legacyDeptColor(j.legacy_dept) || '#90a4ae')) -async function reloadPool () { try { assignPanel.jobs = (await roster.unassignedJobs()).jobs || [] } catch (e) { /* non bloquant */ } } +async function reloadPool () { try { assignPanel.jobs = capPickups((await roster.unassignedJobs()).jobs) } catch (e) { /* non bloquant */ } } // ── Création de job NATIVE OPS (#17) — Dispatch déprécié → tout depuis Planification. Réutilise UnifiedCreateModal (work-order). ── // Job OPS-natif = PAS de legacy_ticket_id → coexiste avec les jobs synchronisés de F (la sync F ne touche que les jobs à legacy_ticket_id). « Sync F transitoire » = coexistence, pas de push F pour l'instant. const createJobOpen = ref(false) @@ -2517,7 +2517,7 @@ function skillIcon (sk) { if (/info|ordinateur|\bpc\b|cam[ée]ra/.test(s)) return 'computer' if (/vente|sales|soumission/.test(s)) return 'sell' if (/factur|paiement|compta/.test(s)) return 'receipt_long' - return 'bolt' + return 'handyman' // visite générique (type non reconnu) : technicien avec outils, plutôt que l'éclair peu parlant } // Icône de compétence pour un MARQUEUR carte (DOM, police material-icons) : UNIQUEMENT des ligatures material (pas les SVG // custom de skillSym) — sinon le glyphe ne s'affiche pas dans un hors q-icon. @@ -2587,7 +2587,7 @@ const draggingJobName = ref(null); const dropCell = ref(null); const dragHours = const selectedJobs = reactive({}) // jobName → true const dropPreview = reactive({ key: null, addH: 0 }) const draggingSet = reactive(new Set()); let _dragGhost = null // jobs en cours de glissé (source estompée) + fantôme custom -async function openAssignPanel () { assignPanel.open = true; assignPanel.loading = true; for (const k in selectedJobs) delete selectedJobs[k]; try { assignPanel.jobs = (await roster.unassignedJobs()).jobs || [] } catch (e) { err(e) } finally { assignPanel.loading = false } } +async function openAssignPanel () { assignPanel.open = true; assignPanel.loading = true; for (const k in selectedJobs) delete selectedJobs[k]; try { assignPanel.jobs = capPickups((await roster.unassignedJobs()).jobs) } catch (e) { err(e) } finally { assignPanel.loading = false } } // Write-back legacy : aperçu (dryRun, 0 écriture) → confirmation → écrit ticket.assign_to dans osTicket. const legacyPush = reactive({ open: false, loading: false, applying: false, preview: null, notify: true }) @@ -4324,7 +4324,14 @@ function setEntryPriority (name, lvl) { // écrit la priorité ERPNext du job (l const solverOpts = reactive({ rankWeight: 2, overtimeCoef: 20, speedKmh: 45, maxSeconds: 8 }) // un COÛT par critère (pas de slider) try { const s = JSON.parse(localStorage.getItem('ops.solverOpts') || 'null'); if (s && typeof s === 'object') Object.assign(solverOpts, s) } catch (e) { /* défauts */ } watch(solverOpts, v => { try { localStorage.setItem('ops.solverOpts', JSON.stringify(v)) } catch (e) {} }, { deep: true }) -function jobDur (j) { return (j && (j.est_min ? j.est_min / 60 : Number(j.duration_h || 1))) || 1 } +// « Ramassage d'équipement » (récupération chez le client) = simple collecte → job court (~5 min). +// On ne surallocue pas le temps même si la classification/est_min du ticket dit plus long. +const PICKUP_RE = /ramm?ass|r[ée]cup[ée]ration/i +const PICKUP_MIN = 5 +function isPickup (j) { return !!j && PICKUP_RE.test(String(j.subject || j.service_type || j.job_type || '')) } +function jobDur (j) { if (isPickup(j)) return PICKUP_MIN / 60; return (j && (j.est_min ? j.est_min / 60 : Number(j.duration_h || 1))) || 1 } +// Cap la durée AFFICHÉE des ramassages à 5 min (le temps affiché suit alors jobDur partout : pool, carte, occupation). +function capPickups (jobs) { return (jobs || []).map(j => isPickup(j) ? { ...j, est_min: PICKUP_MIN } : j) } // Construit un plan job→tech×jour SANS rien écrire (proposition). Chaque job garde sa date si elle est dans la fenêtre. function buildSuggestion () { const useSel = selectedNames.value.length > 0 @@ -5628,7 +5635,7 @@ let _legacyRefreshT = null function scheduleLegacyRefresh () { clearTimeout(_legacyRefreshT) _legacyRefreshT = setTimeout(async () => { - try { await reloadOccupancy(); if (assignPanel.open) { assignPanel.jobs = (await roster.unassignedJobs()).jobs || [] } } catch (e) { /* non bloquant */ } + try { await reloadOccupancy(); if (assignPanel.open) { assignPanel.jobs = capPickups((await roster.unassignedJobs()).jobs) } } catch (e) { /* non bloquant */ } }, 700) } function onLegacyUpdate (data) {