From a7a428f2611082a425081c75f20664be28f88580 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sat, 6 Jun 2026 12:48:44 -0400 Subject: [PATCH] =?UTF-8?q?feat(planif):=20tri=20du=20panneau=20flottant?= =?UTF-8?q?=20=C2=AB=20Jobs=20=C3=A0=20assigner=20=C2=BB=20(groupe/comp?= =?UTF-8?q?=C3=A9tence/date/ville/priorit=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assignSort + assignGroups regroupe/trie selon le mode (défaut = groupe parent-enfant) ; ajout du tri par COMPÉTENCE (demandé) + date/ville/priorité (jobCity = dernier segment adresse ou « Ville | » du sujet) - barre de tri dans le panneau (hors zone de drag) + en-tête de groupe par label ; indentation enfant seulement en mode groupe Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/pages/PlanificationPage.vue | 44 +++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue index d1ff8e6..555a7b4 100644 --- a/apps/ops/src/pages/PlanificationPage.vue +++ b/apps/ops/src/pages/PlanificationPage.vue @@ -539,12 +539,23 @@ +
+ Trier : + +
Chargement…
Aucun job à assigner 🎉
-
Groupe ({{ grp.jobs.length }}) — tout sélectionner (terrain)
-
+
{{ grp.label }} ({{ grp.jobs.length }})
+
Groupe ({{ grp.jobs.length }}) — tout sélectionner (terrain)
+
{{ jobIsOnsite(j) ? 'Sur site (terrain)' : 'À distance / netadmin — pas pour un tech terrain' }} @@ -901,9 +912,29 @@ 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 } } -const assignGroups = computed(() => { // regroupe par parent_job (ou nom propre), ordonné par step_order - const g = {}; for (const j of assignPanel.jobs) { const k = j.parent_job || j.name; (g[k] = g[k] || []).push(j) } - return Object.keys(g).map(k => ({ key: k, jobs: g[k].slice().sort((a, b) => (a.step_order || 0) - (b.step_order || 0)) })) +const assignSort = ref('group') // group (parent-enfant) | skill | date | city | priority +const ASSIGN_PRIO = { urgent: 0, high: 1, medium: 2, low: 3 } +function jobCity (j) { + const a = String(j.location_label || j.service_location || '') + const parts = a.split(',').map(s => s.trim()).filter(Boolean) + if (parts.length >= 2) return parts[parts.length - 1] // dernier segment d'adresse = ville + const subj = String(j.subject || ''); if (subj.includes('|')) return subj.split('|')[0].trim() // sujets legacy « Ville | Nom » + return parts[0] || 'Sans ville' +} +const assignGroups = computed(() => { + const jobs = assignPanel.jobs + if (assignSort.value === 'group') { // défaut : groupe parent-enfant (installation avant activation…), ordonné par step_order + const g = {}; for (const j of jobs) { const k = j.parent_job || j.name; (g[k] = g[k] || []).push(j) } + return Object.keys(g).map(k => ({ key: k, label: null, jobs: g[k].slice().sort((a, b) => (a.step_order || 0) - (b.step_order || 0)) })) + } + const keyOf = j => assignSort.value === 'skill' ? (j.required_skill || 'Sans compétence') + : assignSort.value === 'city' ? jobCity(j) + : assignSort.value === 'priority' ? (j.priority || 'low') + : (j.scheduled_date || 'Sans date') + const labelOf = k => assignSort.value === 'priority' ? (({ urgent: '🔴 Urgent', high: '🟠 Élevée', medium: '🔵 Moyenne', low: '⚪ Basse' })[k] || k) : k + const g = {}; for (const j of jobs) { const k = keyOf(j); (g[k] = g[k] || []).push(j) } + const keys = Object.keys(g).sort((a, b) => assignSort.value === 'priority' ? (ASSIGN_PRIO[a] ?? 9) - (ASSIGN_PRIO[b] ?? 9) : a.localeCompare(b)) + return keys.map(k => ({ key: k, label: labelOf(k), jobs: g[k] })) }) // Terrain vs à distance : l'activation / config / netadmin ne va PAS à un tech sur site (heuristique skill + type/sujet). function jobIsOnsite (j) { @@ -1627,8 +1658,11 @@ onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return /* Panneau flottant « jobs à assigner » (déplaçable, glisser-déposer) */ .assign-panel { position: fixed; z-index: 5000; width: 320px; max-height: 72vh; background: #fff; border: 1px solid #cfd8dc; border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,.24); display: flex; flex-direction: column; } .assign-hdr { display: flex; align-items: center; gap: 5px; padding: 6px 10px; background: #5e35b1; color: #fff; border-radius: 8px 8px 0 0; cursor: move; font-weight: 600; font-size: 13px; user-select: none; } +.assign-sortbar { display: flex; align-items: center; gap: 6px; padding: 4px 10px; font-size: 11px; color: #555; background: #f3f0fa; border-bottom: 1px solid #e0e0e0; } +.assign-sortbar select { font-size: 11px; border: 1px solid #cfc4e8; border-radius: 5px; padding: 1px 4px; background: #fff; color: #333; flex: 1; } .assign-body { overflow: auto; padding: 5px; } .assign-grp { margin-bottom: 6px; border-radius: 7px; padding: 2px; } +.assign-grp-lbl { font-size: 11px; font-weight: 700; color: #37474f; padding: 3px 6px 2px; border-bottom: 1px solid #eee; margin-bottom: 2px; position: sticky; top: 0; background: #fff; z-index: 1; } .assign-grp.grp-hl { background: #ede7f6; box-shadow: inset 0 0 0 1px #b39ddb; } /* groupe lié surligné dès qu'un membre est coché */ .assign-grp-hdr { font-size: 10px; font-weight: 700; color: #5e35b1; padding: 2px 6px; cursor: pointer; display: flex; align-items: center; gap: 3px; } .assign-grp-hdr:hover { text-decoration: underline; }