From 4591ef6169ece0c7e89751377b6db3f49c8e1a38 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 2 Jul 2026 10:50:51 -0400 Subject: [PATCH] feat(dispatch): skill PRIORITY ordering per tech (specialists first) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skill order (not just level) now drives auto-dispatch: index 0 in a tech's skills = their primary function. A job goes to the specialists of that skill first, sparing less-specialized techs for other work (e.g. Louis-Paul does repairs but his primary is sales → repair jobs prefer the repair/install specialists). - buildSuggestion: skillRank = t.skills.indexOf(reqSkill) (0 = primary); score += skillRank * W.rank, tuned per strategy (enough/smart weight specialty highest). Skill capability stays a hard filter. - TagEditor (shared component): chips are now drag-reorderable (vuedraggable, touch-friendly) via `sortable` prop; the 1st chip is marked ★ primary. Reorder emits the reordered array → persists as the ordered skills CSV (order round-trips; stored in custom `skills` field, not Frappe _user_tags, so no alphabetical re-sort). - Skill editor: enable sortable chips + explicit priority numbers (1,2,3…) on the per-skill list, #1 highlighted, plus a hint. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/components/shared/TagEditor.vue | 42 +++++++++++++++----- apps/ops/src/pages/PlanificationPage.vue | 21 ++++++---- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/apps/ops/src/components/shared/TagEditor.vue b/apps/ops/src/components/shared/TagEditor.vue index 03e3116..acabb11 100644 --- a/apps/ops/src/components/shared/TagEditor.vue +++ b/apps/ops/src/components/shared/TagEditor.vue @@ -12,6 +12,7 @@ */ import { ref, computed, nextTick, watch, onMounted } from 'vue' import { useQuasar } from 'quasar' +import draggable from 'vuedraggable' // réordonnancement des chips (priorité de compétence) — glisser-déposer, marche au doigt const $q = useQuasar() const isDark = computed(() => $q.dark.isActive) @@ -29,6 +30,7 @@ const props = defineProps({ showRequired:{ type: Boolean, default: false }, // show required pin per tag (jobs) compact: { type: Boolean, default: false }, // smaller chips autofocus: { type: Boolean, default: false }, // focus l'input au montage (ouvre la liste direct) + sortable: { type: Boolean, default: false }, // glisser-déposer pour réordonner les chips (ORDRE = priorité) ; 1er = principal }) const emit = defineEmits(['update:modelValue', 'create', 'update-tag', 'rename-tag', 'delete-tag']) @@ -36,6 +38,9 @@ const emit = defineEmits(['update:modelValue', 'create', 'update-tag', 'rename-t function _labels () { return props.modelValue.map(v => typeof v === 'string' ? v : v.tag) } +function _lbl (v) { return typeof v === 'string' ? v : (v && v.tag) } // libellé d'un item (string ou {tag}) +function _itemKey (v) { return _lbl(v) } +function onReorder (v) { emit('update:modelValue', v) } // glisser-déposer → nouvel ordre = nouvelle priorité (émis tel quel, préserve level/required) function _getLevel (label) { const item = props.modelValue.find(v => typeof v !== 'string' && v.tag === label) return item?.level ?? 0 @@ -187,17 +192,25 @@ onMounted(() => { if (props.autofocus) nextTick(() => { inputEl.value?.focus();