From 91456249493ba05e3148c089f107d6b52fed7867 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 9 Jul 2026 14:57:01 -0400 Subject: [PATCH] feat(ops): unify tech skill/cadence editor + merge team tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/planif/SkillCadenceTable.vue | 97 ++++++++++++ apps/ops/src/pages/PlanificationPage.vue | 149 +++++++++++------- 2 files changed, 187 insertions(+), 59 deletions(-) create mode 100644 apps/ops/src/components/planif/SkillCadenceTable.vue diff --git a/apps/ops/src/components/planif/SkillCadenceTable.vue b/apps/ops/src/components/planif/SkillCadenceTable.vue new file mode 100644 index 0000000..c9cb14a --- /dev/null +++ b/apps/ops/src/components/planif/SkillCadenceTable.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index 35d26fe..fbd63dd 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -76,8 +76,7 @@ Point de départ (dépôt){{ depot && depot.address ? depot.address : 'non défini — cliquer pour situer' }} - Gérer les compétences (tags) - Cadence équipe + Équipe — compétences · cadence · coûtCompétences (score + cadence/skill) · coût · catalogue Durées par caractéristique @@ -614,25 +613,73 @@ - -
Équipe — cadence & coût
+ +
Équipe — compétences, cadence & coût
-
Cadence : 1.00 normal · 1.10 = +10 % (plus lent) · 0.90 = −10 % (plus rapide). Coût chargé/h = salaire × (1 + charges %) + autres (véhicule, outils, frais). Le solveur préfère les techs rapides et moins coûteux.
-
- - - - - - - - - - - - - -
TechnicienCompétencesEfficacité %Salaire/hCharges %Autres/hCoût chargé/h
{{ t.name }}{{ t.group }}100 % = cadence normale · PLUS HAUT = plus rapide (fait plus de jobs). Ex. 300 % = 3× plus rapide · 60 % = plus lent.{{ loadedCost(t) }} $
+ + +
+
Aucune compétence — ajoute-en via un technicien ci-dessous.
+ + + + Couleur +
+
+
Perso
+
+
+
+ + {{ tg.label }} + + + + + {{ tg.count }} technicien(s) · clic = renommer + + Supprimer partout +
+
+
+
+ + +
Clique un technicien pour régler ses compétences (ordre = priorité), son score ★ et sa cadence par compétence (défaut global 100 %), plus son coût. Enregistrement automatique.
+
+ + + +
+ + +
💵 Coût chargé/h = salaire × (1 + charges %) + autres
+
+ + + +
= {{ loadedCostRow(t.id) }} $/h
+
+
+
+
@@ -733,43 +780,13 @@
🏷 {{ skillDialog.name }}
- -
Glisse les chips pour l'ordre de priorité — la 1re (★) = fonction principale : le dispatch auto envoie ces jobs à ce tech en premier (épargne les moins spécialisés).
-
-
-
Compétence
-
Score
-
Cadence
-
-
-
- {{ si + 1 }} - Couleur -
-
- -
-
- Perso - - toute couleur -
-
-
- {{ sk }} -
-
- -
-
- -
-
-
Score ★ = maîtrise · Cadence : 100 % = normale · 200 % = deux fois plus de jobs · < 100 % = plus lent · vide = hérite du global · × sur le chip = retirer.
-
Enregistré automatiquement — pas de bouton à cliquer.
-
+ + +
Enregistré automatiquement — pas de bouton à cliquer.
🗓 Horaire
@@ -1915,6 +1932,7 @@ import TicketStatusControl from 'src/components/shared/TicketStatusControl.vue' import HelpHint from 'src/components/shared/HelpHint.vue' import { skillIcon, skillSym, markerIcon } from 'src/composables/useSkillIcons' // SOURCE UNIQUE des icônes de compétences (réutilisée fiche client) import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags/compétences PARTAGÉ (chips colorées, création + palette, niveaux) — SOURCE UNIQUE : jobs ET techs +import SkillCadenceTable from 'src/components/planif/SkillCadenceTable.vue' // éditeur compétences+cadence PARTAGÉ (volet tech + outil Équipe) import RouteMap from 'src/components/shared/RouteMap.vue' // carte de tournées réutilisable (revue dispatch auto + onglet Tournées) : OSRM réel, arrêts numérotés, fitTo import OccupancyStrip from 'src/components/shared/OccupancyStrip.vue' // bande d'occupation réutilisable (sélecteur de jour Tournées + tableau de bord) import OverflowMenu from 'src/components/shared/OverflowMenu.vue' // « ⋮ » standardisé pour les actions secondaires (désencombrer la barre) @@ -2046,7 +2064,7 @@ function isHidden (id) { return hiddenTechs.value.includes(id) } function toggleHidden (id) { const i = hiddenTechs.value.indexOf(id); if (i >= 0) hiddenTechs.value.splice(i, 1); else hiddenTechs.value.push(id); localStorage.setItem('roster-hidden-techs-v1', JSON.stringify(hiddenTechs.value)) } const hiddenCount = computed(() => techs.value.filter(t => isHidden(t.id)).length) const showShiftEditor = ref(false) // v-model de (editTpls/newTpl/fonctions déplacés dans le composant) -const showTeamEditor = ref(false); const editTechs = ref([]) +const showTeamEditor = ref(false); const editTechs = ref([]); const teamCost = reactive({}) const notifySms = ref(false) // ── Notifier les techs de leur tournée (SMS/Email + lien token) : aperçu d'abord (compose sans envoi), puis envoi. ── const notifyDlg = reactive({ open: false, loading: false, sending: false, techs: [] }) @@ -5715,7 +5733,20 @@ function techRole (t) { } function roleIcon (t) { const r = techRole(t); return r ? ROLE_ICON[r] : null } function roleLabel (t) { const r = techRole(t); return r ? ROLE_LABEL[r] : '' } -function openTeamEditor () { editTechs.value = techs.value.map(t => ({ id: t.id, name: t.name, group: t.group, skills: [...(t.skills || [])], efficiency: t.efficiency || 1, salary: t.cost_salary_h || 0, charges: t.cost_charges_pct || 0, other: t.cost_other_h || 0 })); showTeamEditor.value = true } +// Opère sur les objets tech LIVE (techs.value) → SkillCadenceTable + les handlers de compétence/cadence persistent +// directement (skill_levels/skill_eff/skills/efficiency). Le coût passe par un tampon (teamCost) sauvé au blur. +function openTeamEditor () { + editTechs.value = techs.value + for (const t of techs.value) teamCost[t.id] = { salary: t.cost_salary_h || 0, charges: t.cost_charges_pct || 0, other: t.cost_other_h || 0 } + showTeamEditor.value = true +} +function loadedCostRow (id) { const c = teamCost[id] || {}; return Math.round(((Number(c.salary) || 0) * (1 + (Number(c.charges) || 0) / 100) + (Number(c.other) || 0)) * 100) / 100 } +async function saveCostRow (t) { + const c = teamCost[t.id] || {} + try { await roster.setTechCost(t.id, { salary: c.salary, charges: c.charges, other: c.other }); t.cost_salary_h = Number(c.salary) || 0; t.cost_charges_pct = Number(c.charges) || 0; t.cost_other_h = Number(c.other) || 0; t.cost_h = loadedCostRow(t.id); $q.notify({ type: 'positive', message: t.name + ' : ' + loadedCostRow(t.id) + ' $/h chargé' }) } catch (e) { err(e) } +} +// Cadence GLOBALE du tech (émise par SkillCadenceTable, en %) → facteur + persiste (défaut 100 % ↔ facteur 1). +function setGlobalCadence (t, pct) { t.efficiency = factorFromPct(pct); saveEff(t) } // Éditeur d'équipe (table) : TagEditor émet un tableau → t.skills en tableau, puis persiste (l'API attend une CSV). function onTeamSkills (t, items) { t.skills = normSkillList(items); saveSkills(t) } async function saveSkills (t) { const csv = normSkillList(t.skills).join(','); try { await roster.setTechSkills(t.id, csv); const tt = techs.value.find(x => x.id === t.id); if (tt) tt.skills = csv ? csv.split(',') : []; $q.notify({ type: 'positive', message: t.name + ' : compétences enregistrées' }) } catch (e) { err(e) } }