diff --git a/apps/ops/src/components/shared/SkillSelect.vue b/apps/ops/src/components/shared/SkillSelect.vue deleted file mode 100644 index c19b9e7..0000000 --- a/apps/ops/src/components/shared/SkillSelect.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index a6498fa..f03bf63 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -195,7 +195,7 @@ - + @@ -394,7 +394,7 @@ {{ s.label }}
Compétences requises
- +
Reporter (date prévue)
@@ -700,7 +700,7 @@ {{ t.name }}{{ t.group }} - + 100 % = cadence normale · PLUS HAUT = plus rapide (fait plus de jobs). Ex. 300 % = 3× plus rapide · 60 % = plus lent. @@ -1490,7 +1490,7 @@
Compétences requises - +
@@ -2056,8 +2056,7 @@ import { useSla, SLA_BADGE } from 'src/composables/useSla' // SLA existant (poli import { useAuthStore } from 'src/stores/auth' // email de l'agent (X-Authentik-Email) pour l'envoi de réponses client import TechSelect from 'src/components/shared/TechSelect.vue' import TicketStatusControl from 'src/components/shared/TicketStatusControl.vue' -import SkillSelect from 'src/components/shared/SkillSelect.vue' -import TagEditor from 'src/components/shared/TagEditor.vue' // module de tags partagé (Dispatch) : condensé, création à la volée, couleurs +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 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 UnifiedCreateModal from 'src/components/shared/UnifiedCreateModal.vue' // création de job NATIVE OPS (work-order) — Dispatch déprécié, tout ici @@ -2502,12 +2501,18 @@ const geoTimeline = computed(() => { function techNameById (id) { const t = (techs.value || []).find(x => x.id === id); return t ? t.name : '' } // repli nom d'un assistant depuis l'id // Nom d'assistant ROBUSTE : une vieille ligne a pu être persistée avec tech_name = la CHAÎNE « undefined »/« null » → on la traite comme vide et on résout par l'id. function jdAssistantName (a) { const bad = v => !v || v === 'undefined' || v === 'null'; if (a && !bad(a.tech_name)) return a.tech_name; const n = techNameById(a && a.tech_id); return n || (a && !bad(a.tech_id) ? a.tech_id : '—') } -// Éditeur de COMPÉTENCES REQUISES du job (SkillSelect multi + création) + attribution à un tech (dispatch auto). +// Éditeur de COMPÉTENCES REQUISES du job (TagEditor partagé : chips colorées + création + palette) + attribution à un tech (dispatch auto). const jdSkillTech = ref(null) const jdSkillBusy = ref(false) +// Normalise la sortie d'un éditeur de compétences → libellés propres dédupliqués. +// TagEditor émet un tableau de libellés (ou de {tag}) ; on tolère aussi une CSV (rétro-compat). +function normSkillList (list) { + const raw = Array.isArray(list) ? list : String(list || '').split(',') + return [...new Set(raw.map(x => (typeof x === 'string' ? x : (x && x.tag) || '')).map(s => String(s).trim()).filter(Boolean))] +} // LISTE de compétences requises du job → store hub /roster/job-skills (PAS un champ ERPNext). required_skill(principale)=1re. Le tech doit les avoir TOUTES. async function jdSetJobSkills (list) { - const arr = [...new Set((Array.isArray(list) ? list : String(list || '').split(',')).map(s => String(s).trim()).filter(Boolean))] // SkillSelect émet une CSV ("a,b"), pas un tableau + const arr = normSkillList(list) jobDetail.skills = arr; jobDetail.skill = arr[0] || '' if (jobDetail.name) { try { await roster.setJobSkills(jobDetail.name, arr); if (typeof reloadPool === 'function') reloadPool() } catch (e) { err(e) } } } @@ -5502,10 +5507,10 @@ async function patchJob (j, patch, okMsg) { function toggleUrgent (j) { const on = j.priority === 'high'; patchJob(j, { priority: on ? 'medium' : 'high' }, on ? 'Priorité moyenne' : '⚡ Urgent') } function setJobPriority (j, p) { patchJob(j, { priority: p }, 'Priorité : ' + ((POOL_PRIOS.find(x => x.value === p) || {}).label || p)) } function setJobStatus (j, s) { patchJob(j, { status: s }, 'Statut : ' + ((POOL_STATUSES.find(x => x.value === s) || {}).label || s)) } -// Compétences requises (LISTE) du job depuis la feuille du pool → store hub /roster/job-skills (PAS un champ ERPNext). SkillSelect émet une CSV. Optimiste + réconcilie sur échec. +// Compétences requises (LISTE) du job depuis la feuille du pool → store hub /roster/job-skills (PAS un champ ERPNext). Optimiste + réconcilie sur échec. async function setPoolSkills (j, list) { if (!j) return - const arr = [...new Set((Array.isArray(list) ? list : String(list || '').split(',')).map(s => String(s).trim()).filter(Boolean))] + const arr = normSkillList(list) j.required_skills = arr; j.required_skill = arr[0] || '' // affichage/couleur/icône immédiats if (!j.name) return try { await roster.setJobSkills(j.name, arr); $q.notify({ type: 'positive', message: 'Compétences : ' + (arr.join(', ') || '—'), timeout: 1600 }) } @@ -5788,8 +5793,10 @@ 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 || []).join(', '), 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 } -async function saveSkills (t) { try { await roster.setTechSkills(t.id, t.skills || ''); const tt = techs.value.find(x => x.id === t.id); if (tt) tt.skills = (t.skills || '').split(',').map(s => s.trim()).filter(Boolean); $q.notify({ type: 'positive', message: t.name + ' : compétences enregistrées' }) } catch (e) { err(e) } } +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 } +// É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) } } // congés / disponibilités async function openLeave () { showLeave.value = true; await loadLeave() } @@ -5865,7 +5872,7 @@ async function doWeekStatus (mode) { } // demande -function loadLS () { try { demand.value = JSON.parse(localStorage.getItem(LS_DEMAND) || '[]') } catch { demand.value = [] } try { holidays.value = JSON.parse(localStorage.getItem(LS_HOL) || '[]') } catch { holidays.value = [] } try { weekTemplates.value = JSON.parse(localStorage.getItem(LS_TPL) || '[]') } catch { weekTemplates.value = [] } try { gardeRules.value = JSON.parse(localStorage.getItem(LS_GARDE) || '[]') } catch { gardeRules.value = [] } try { manualGarde.value = JSON.parse(localStorage.getItem(LS_GARDE_MANUAL) || '{}') } catch { manualGarde.value = {} } try { customTags.value = JSON.parse(localStorage.getItem('roster-skill-tags-v1') || '[]') } catch { customTags.value = [] } try { hiddenTechs.value = JSON.parse(localStorage.getItem('roster-hidden-techs-v1') || '[]') } catch { hiddenTechs.value = [] } } +function loadLS () { try { demand.value = JSON.parse(localStorage.getItem(LS_DEMAND) || '[]') } catch { demand.value = [] } demand.value.forEach(d => { if (!Array.isArray(d.skills)) d.skills = String(d.skills || '').split(',').map(s => s.trim()).filter(Boolean) }); /* migration CSV→tableau (ancien SkillSelect) */ try { holidays.value = JSON.parse(localStorage.getItem(LS_HOL) || '[]') } catch { holidays.value = [] } try { weekTemplates.value = JSON.parse(localStorage.getItem(LS_TPL) || '[]') } catch { weekTemplates.value = [] } try { gardeRules.value = JSON.parse(localStorage.getItem(LS_GARDE) || '[]') } catch { gardeRules.value = [] } try { manualGarde.value = JSON.parse(localStorage.getItem(LS_GARDE_MANUAL) || '{}') } catch { manualGarde.value = {} } try { customTags.value = JSON.parse(localStorage.getItem('roster-skill-tags-v1') || '[]') } catch { customTags.value = [] } try { hiddenTechs.value = JSON.parse(localStorage.getItem('roster-hidden-techs-v1') || '[]') } catch { hiddenTechs.value = [] } } // ── Rotation de garde par département (récurrence + rotation) ──────────────── const GARDE_EPOCH = '2026-01-05' // lundi de référence pour l'index de semaine @@ -5959,7 +5966,8 @@ async function applyGardeRules () { } catch (e) { err(e) } } function saveDemand () { localStorage.setItem(LS_DEMAND, JSON.stringify(demand.value)) } -function addDemand () { demand.value = [...demand.value, { shift: templates.value[0] && templates.value[0].name, zone: 'Montréal', skills: '', job_h: 0, weekday: 1, weekend: 0, holiday: 0 }]; saveDemand() } +function onDemandSkills (d, items) { d.skills = normSkillList(items); saveDemand() } // TagEditor émet un tableau ; d.skills reste un tableau (→ CSV à la génération) +function addDemand () { demand.value = [...demand.value, { shift: templates.value[0] && templates.value[0].name, zone: 'Montréal', skills: [], job_h: 0, weekday: 1, weekend: 0, holiday: 0 }]; saveDemand() } function removeDemand (i) { demand.value = demand.value.filter((_, j) => j !== i); saveDemand() } async function applyDemand () { if (!demand.value.length) { $q.notify({ type: 'warning', message: 'Aucune ligne de demande' }); return } @@ -5974,7 +5982,7 @@ async function applyDemand () { const jobH = Number(row.job_h) || 0 const sh = (tplByName.value[row.shift] && tplByName.value[row.shift].hours) || 8 const count = jobH > 0 ? Math.max(1, Math.ceil(n * jobH / sh)) : n // mode jobs → effectif - reqs.push({ requirement_date: d.iso, shift_template: row.shift, zone: row.zone || '', required_count: count, required_skills: row.skills || '' }) + reqs.push({ requirement_date: d.iso, shift_template: row.shift, zone: row.zone || '', required_count: count, required_skills: normSkillList(row.skills).join(',') }) } } if (reqs.length) await roster.bulkRequirements(reqs)