feat(dispatch): P1-A SLA on Planification jobs — reuse existing SLA system
SLA was already built (missed by Gemini's plan): hub /sla/policies store, useSla() composable (slaFor → ok/at_risk/breached + SLA_BADGE), editable SlaSettings.vue — used on tickets. Surfaced it on Planification jobs by reuse (NO ERPNext schema migration, NO new cron): - jobSlaBadge(job): maps a Dispatch Job to a synthetic ticket (priority→capitalized, creation, first_responded_on set so it uses the RESOLUTION band = creation + policy[priority]) and returns only the actionable states. entrySlaBadge for review entries via poolByName. - Red "SLA dépassé" / orange "SLA proche" chip on pool jobs (triage) and review entries; ok/none show nothing (no clutter). Compute-on-read, so the matrix stays editable in Settings with zero migration risk. Verified: 57 breached + 5 at-risk on the current pool backlog; chip renders next to the job subject. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3599ec995f
commit
28dff24227
|
|
@ -923,6 +923,8 @@
|
|||
<span v-if="groupLabel(j)" class="assign-grp-badge q-mr-xs"><q-tooltip class="bg-grey-9">Même adresse de service (groupe {{ groupLabel(j)[0] }}) — simple repère de proximité</q-tooltip>{{ groupLabel(j) }}</span>
|
||||
<q-badge v-if="j.step_order" color="indigo" class="q-mr-xs">{{ j.step_order }}</q-badge>
|
||||
<span class="ellipsis text-weight-medium" :style="assignPanel.showMap ? 'cursor:pointer' : ''" @click="assignPanel.showMap && focusAssignJob(j)">{{ j.subject || j.service_type || j.name }}<q-tooltip v-if="j.legacy_detail" max-width="380px" class="bg-grey-9" style="white-space:pre-wrap;font-size:11px">{{ j.legacy_detail }}</q-tooltip></span>
|
||||
<!-- SLA : pastille rouge (dépassé) / orange (proche) — réutilise les politiques SLA des tickets -->
|
||||
<span v-if="jobSlaBadge(j)" class="assign-sla" :class="'sla-' + jobSlaBadge(j).state"><q-icon :name="jobSlaBadge(j).icon" size="11px" />{{ jobSlaBadge(j).short }}<q-tooltip class="bg-grey-9">{{ jobSlaBadge(j).label }}</q-tooltip></span>
|
||||
<q-space />
|
||||
<q-btn v-if="j.status !== 'On Hold'" flat dense round size="sm" icon="person_add" color="primary" class="q-ml-xs" @click.stop @mousedown.stop>
|
||||
<q-tooltip>Assigner à un technicien (classés par pertinence : compétence · dispo · distance · charge)</q-tooltip>
|
||||
|
|
@ -1115,6 +1117,7 @@
|
|||
<span v-if="e.skill" class="assign-skill" :style="{ background: getTagColor(e.skill) }">{{ e.skill }}</span>
|
||||
<span v-if="e.lvl" class="suggest-mlvl" :title="'Maîtrise du tech : ' + e.lvl + (e.reqLevel > 1 ? ' / niveau requis ' + e.reqLevel : '')">★{{ e.lvl }}<span v-if="e.reqLevel > 1" style="opacity:.6">/{{ e.reqLevel }}</span></span>
|
||||
<q-icon v-if="e.eff && e.eff < 0.95" name="bolt" size="12px" color="green-6" class="q-mr-xs"><q-tooltip>Tech rapide sur ce type ({{ e.eff }})</q-tooltip></q-icon>
|
||||
<span v-if="entrySlaBadge(e)" class="assign-sla q-mr-xs" :class="'sla-' + entrySlaBadge(e).state"><q-icon :name="entrySlaBadge(e).icon" size="11px" />{{ entrySlaBadge(e).short }}<q-tooltip class="bg-grey-9">{{ entrySlaBadge(e).label }}</q-tooltip></span>
|
||||
<!-- Coordination d'adresse : 🔗 teal = épinglé sur le tech qui a DÉJÀ un job à cette adresse · 🔗 indigo = N jobs du pool fusionnés en 1 arrêt -->
|
||||
<q-icon v-if="e.coordWith" name="link" size="14px" color="teal-7" class="q-mr-xs"><q-tooltip>Coordonné : même adresse que « {{ e.coordWith }} » déjà assigné à {{ e.techName }} — placé sur SON arrêt</q-tooltip></q-icon>
|
||||
<q-btn v-else-if="e.sameAddrN > 1" flat dense round size="xs" :icon="e.grouped ? 'inventory_2' : 'link'" :color="e.grouped ? 'indigo-7' : 'indigo-4'" class="q-mr-xs">
|
||||
|
|
@ -1755,6 +1758,7 @@ import { legacyDeptColor } from 'src/composables/useHelpers' // coloriage par ty
|
|||
import { useUserPrefs } from 'src/composables/useUserPrefs' // préférences d'affichage par utilisateur (serveur)
|
||||
import { relTime } from 'src/composables/useFormatters' // temps relatif COHÉRENT (même formateur que la Boîte)
|
||||
import { messageIdentity, priorityMeta, PRIORITY_LEVELS } from 'src/composables/useConversationDisplay' // resolvers partagés + priorité (drapeau) réutilisée des conversations
|
||||
import { useSla, SLA_BADGE } from 'src/composables/useSla' // SLA existant (politiques éditables) — réutilisé sur les jobs (échéance = création + résolution)
|
||||
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 SkillSelect from 'src/components/shared/SkillSelect.vue'
|
||||
|
|
@ -4113,6 +4117,20 @@ async function groupSameAddr (e, g, d) {
|
|||
// PRIORITÉ du job — MÊME drapeau que les conversations (priorityMeta) + champ ERPNext STANDARD (low/medium/high) via patchJob, comme la fiche du pool. Une seule source.
|
||||
const poolPriority = computed(() => { const m = {}; for (const j of (assignPanel.jobs || [])) m[j.name] = String(j.priority || '').toLowerCase(); return m })
|
||||
function jobPriorityOf (name) { return poolPriority.value[name] || '' } // medium → priorityMeta = contour gris (non classé)
|
||||
// ── SLA sur les jobs (RÉUTILISE useSla des tickets) : échéance de RÉSOLUTION = création + politique[priorité]. Compute-on-read, aucune migration ERPNext. ──
|
||||
const poolByName = computed(() => { const m = {}; for (const j of (assignPanel.jobs || [])) m[j.name] = j; return m })
|
||||
const { slaFor: _slaForTicket } = useSla()
|
||||
const PRIO_CAP = { high: 'High', 'élevée': 'High', elevee: 'High', urgent: 'Urgent', medium: 'Medium', moyenne: 'Medium', normal: 'Medium', low: 'Low', basse: 'Low' }
|
||||
function jobSlaBadge (job) { // { state, color, text, icon, label } pour at_risk/breached SEULEMENT (états actionnables) ; null sinon
|
||||
if (!job || !job.creation) return null
|
||||
const prio = PRIO_CAP[String(job.priority || jobPriorityOf(job.name) || 'medium').toLowerCase()] || 'Medium'
|
||||
// job → ticket synthétique : first_responded_on renseigné → on SAUTE la bande « réponse » et on prend la bande RÉSOLUTION depuis la création.
|
||||
const s = _slaForTicket({ issue_type: job.job_type || undefined, priority: prio, creation: job.creation, first_responded_on: job.creation, status: (String(job.status || '').toLowerCase() === 'completed' ? 'Closed' : job.status) })
|
||||
if (!s || (s.state !== 'at_risk' && s.state !== 'breached')) return null
|
||||
const b = SLA_BADGE[s.state]
|
||||
return { state: s.state, color: b.color, text: b.text, icon: b.icon, label: s.label, short: s.state === 'breached' ? 'SLA dépassé' : 'SLA proche' }
|
||||
}
|
||||
function entrySlaBadge (e) { return jobSlaBadge(poolByName.value[e.jobName]) } // revue : retrouve le job du pool pour l'échéance
|
||||
function setEntryPriority (name, lvl) { // écrit la priorité ERPNext du job (low/medium/high) — réutilise patchJob (optimiste + updateJob)
|
||||
const j = (assignPanel.jobs || []).find(x => x.name === name); if (!j) return
|
||||
patchJob(j, { priority: lvl || 'medium' }, 'Priorité : ' + priorityMeta(lvl).label)
|
||||
|
|
@ -5401,6 +5419,9 @@ onBeforeRouteLeave(() => { if (dirty.value && !window.confirm(DIRTY_MSG)) return
|
|||
.suggest-dur:hover { color: #4f46e5; border-bottom-color: #4f46e5; }
|
||||
.suggest-leg-time { font-size: 10px; color: #94a3b8; padding: 0 6px 2px 30px; line-height: 1.2; }
|
||||
.suggest-leg-return { color: #64748b; font-style: italic; padding-top: 2px; border-top: 1px dashed #e2e8f0; margin-top: 1px; }
|
||||
.assign-sla { display: inline-flex; align-items: center; gap: 2px; font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: 8px; white-space: nowrap; margin-left: 4px; }
|
||||
.assign-sla.sla-breached { background: #fee2e2; color: #b91c1c; }
|
||||
.assign-sla.sla-at_risk { background: #ffedd5; color: #c2410c; }
|
||||
.rt-chip { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border: 1.5px solid #cbd5e1; border-radius: 14px; font-size: 12px; font-weight: 600; color: #334155; cursor: pointer; user-select: none; background: #fff; }
|
||||
.rt-chip.off { opacity: 0.55; border-style: dashed; }
|
||||
.rt-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user