feat(planif): tri du panneau flottant « Jobs à assigner » (groupe/compétence/date/ville/priorité)
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
368e22d57e
commit
a7a428f261
|
|
@ -539,12 +539,23 @@
|
||||||
<q-btn flat dense round size="sm" icon="refresh" color="white" :loading="assignPanel.loading" @click="openAssignPanel" />
|
<q-btn flat dense round size="sm" icon="refresh" color="white" :loading="assignPanel.loading" @click="openAssignPanel" />
|
||||||
<q-btn flat dense round size="sm" icon="close" color="white" @click="assignPanel.open = false" />
|
<q-btn flat dense round size="sm" icon="close" color="white" @click="assignPanel.open = false" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="assign-sortbar" @mousedown.stop>
|
||||||
|
<span>Trier :</span>
|
||||||
|
<select v-model="assignSort" @mousedown.stop>
|
||||||
|
<option value="group">Groupe (parent-enfant)</option>
|
||||||
|
<option value="skill">Compétence</option>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
<option value="city">Ville</option>
|
||||||
|
<option value="priority">Priorité</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="assign-body">
|
<div class="assign-body">
|
||||||
<div v-if="assignPanel.loading" class="text-grey-6 q-pa-md text-center">Chargement…</div>
|
<div v-if="assignPanel.loading" class="text-grey-6 q-pa-md text-center">Chargement…</div>
|
||||||
<div v-else-if="!assignPanel.jobs.length" class="text-grey-6 q-pa-md text-center">Aucun job à assigner 🎉</div>
|
<div v-else-if="!assignPanel.jobs.length" class="text-grey-6 q-pa-md text-center">Aucun job à assigner 🎉</div>
|
||||||
<div v-for="grp in assignGroups" :key="grp.key" class="assign-grp" :class="{ 'grp-hl': groupSelected(grp) }">
|
<div v-for="grp in assignGroups" :key="grp.key" class="assign-grp" :class="{ 'grp-hl': groupSelected(grp) }">
|
||||||
<div v-if="grp.jobs.length > 1" class="assign-grp-hdr" @click="toggleGroupSel(grp)"><q-icon name="account_tree" size="12px" /> Groupe ({{ grp.jobs.length }}) — tout sélectionner (terrain)</div>
|
<div v-if="grp.label" class="assign-grp-lbl">{{ grp.label }} <span style="opacity:.6">({{ grp.jobs.length }})</span></div>
|
||||||
<div v-for="(j, idx) in grp.jobs" :key="j.name" class="assign-job" :class="{ blocked: j.status === 'On Hold', child: grp.jobs.length > 1 && idx > 0, sel: !!selectedJobs[j.name], dragging: draggingSet.has(j.name) }" :style="{ borderLeft: '5px solid ' + panelJobColor(j) }" draggable="true" @dragstart="onJobDragStart($event, j)" @dragend="onJobDragEnd">
|
<div v-if="assignSort === 'group' && grp.jobs.length > 1" class="assign-grp-hdr" @click="toggleGroupSel(grp)"><q-icon name="account_tree" size="12px" /> Groupe ({{ grp.jobs.length }}) — tout sélectionner (terrain)</div>
|
||||||
|
<div v-for="(j, idx) in grp.jobs" :key="j.name" class="assign-job" :class="{ blocked: j.status === 'On Hold', child: assignSort === 'group' && grp.jobs.length > 1 && idx > 0, sel: !!selectedJobs[j.name], dragging: draggingSet.has(j.name) }" :style="{ borderLeft: '5px solid ' + panelJobColor(j) }" draggable="true" @dragstart="onJobDragStart($event, j)" @dragend="onJobDragEnd">
|
||||||
<div class="row items-center no-wrap">
|
<div class="row items-center no-wrap">
|
||||||
<q-checkbox dense size="xs" :model-value="!!selectedJobs[j.name]" @update:model-value="selectedJobs[j.name] = $event" @click.stop @mousedown.stop class="q-mr-xs" />
|
<q-checkbox dense size="xs" :model-value="!!selectedJobs[j.name]" @update:model-value="selectedJobs[j.name] = $event" @click.stop @mousedown.stop class="q-mr-xs" />
|
||||||
<q-icon :name="jobIsOnsite(j) ? 'home_repair_service' : 'cloud'" size="13px" :color="jobIsOnsite(j) ? 'teal' : 'grey-5'" class="q-mr-xs"><q-tooltip>{{ jobIsOnsite(j) ? 'Sur site (terrain)' : 'À distance / netadmin — pas pour un tech terrain' }}</q-tooltip></q-icon>
|
<q-icon :name="jobIsOnsite(j) ? 'home_repair_service' : 'cloud'" size="13px" :color="jobIsOnsite(j) ? 'teal' : 'grey-5'" class="q-mr-xs"><q-tooltip>{{ jobIsOnsite(j) ? 'Sur site (terrain)' : 'À distance / netadmin — pas pour un tech terrain' }}</q-tooltip></q-icon>
|
||||||
|
|
@ -901,9 +912,29 @@ const selectedJobs = reactive({}) // jobName → true
|
||||||
const dropPreview = reactive({ key: null, addH: 0 })
|
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
|
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 = (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 assignSort = ref('group') // group (parent-enfant) | skill | date | city | priority
|
||||||
const g = {}; for (const j of assignPanel.jobs) { const k = j.parent_job || j.name; (g[k] = g[k] || []).push(j) }
|
const ASSIGN_PRIO = { urgent: 0, high: 1, medium: 2, low: 3 }
|
||||||
return Object.keys(g).map(k => ({ key: k, jobs: g[k].slice().sort((a, b) => (a.step_order || 0) - (b.step_order || 0)) }))
|
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).
|
// Terrain vs à distance : l'activation / config / netadmin ne va PAS à un tech sur site (heuristique skill + type/sujet).
|
||||||
function jobIsOnsite (j) {
|
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) */
|
/* 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-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-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-body { overflow: auto; padding: 5px; }
|
||||||
.assign-grp { margin-bottom: 6px; border-radius: 7px; padding: 2px; }
|
.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.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 { 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; }
|
.assign-grp-hdr:hover { text-decoration: underline; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user