diff --git a/apps/ops/src/api/roster.js b/apps/ops/src/api/roster.js
index 745b0a9..d6e3384 100644
--- a/apps/ops/src/api/roster.js
+++ b/apps/ops/src/api/roster.js
@@ -53,7 +53,6 @@ export const askAssistant = (message, history) => jpost('/roster/assistant', { m
export const getPolicy = () => jget('/roster/policy')
export const savePolicy = (policy) => jpost('/roster/policy', policy)
export const setJobLevel = (name, level) => jpost('/roster/job-level', { name, level }) // niveau requis persistant par job
-export const setJobFlag = (name, priority) => jpost('/roster/job-flag', { name, priority }) // priorité de dispatch persistante par job (SLA/commercial)
export async function deleteShiftTemplate (name) {
const r = await fetch(HUB + '/roster/template/' + encodeURIComponent(name), { method: 'DELETE' })
if (!r.ok) throw new Error('Suppression modèle: ' + r.status)
diff --git a/apps/ops/src/composables/useConversationDisplay.js b/apps/ops/src/composables/useConversationDisplay.js
index 1caf3d3..71e8b35 100644
--- a/apps/ops/src/composables/useConversationDisplay.js
+++ b/apps/ops/src/composables/useConversationDisplay.js
@@ -67,15 +67,17 @@ export function channelMeta (channel) {
return CHANNELS[channel] || { icon: 'chat_bubble', color: 'grey-5', label: channel || 'Message' }
}
-// priorityMeta(p) → { icon, color, label, rank } pour le DRAPEAU de priorité (style ClickUp), cohérent liste + détail.
-// rank = tri (urgent en haut). '' / 'none' = aucune priorité → drapeau contour discret (invite à classer).
+// priorityMeta(p) → { icon, color, label, rank } pour le DRAPEAU de priorité, cohérent liste + détail, PARTOUT (conversations + jobs).
+// STANDARD 3 NIVEAUX (comme ERPNext) : haute (ROUGE) · moyenne (neutre gris) · basse. '' / medium = non classé → drapeau contour gris (invite à classer).
const PRIORITIES = {
- urgent: { icon: 'flag', color: 'red-6', label: 'Urgent', rank: 4 },
- high: { icon: 'flag', color: 'orange-7', label: 'Haute', rank: 3 },
- normal: { icon: 'flag', color: 'blue-6', label: 'Normale', rank: 2 },
+ high: { icon: 'flag', color: 'red-6', label: 'Haute', rank: 3 },
+ medium: { icon: 'outlined_flag', color: 'grey-5', label: 'Moyenne', rank: 2 }, // défaut ERPNext → contour gris (non classé)
low: { icon: 'flag', color: 'blue-grey-5', label: 'Basse', rank: 1 },
+ // rétro-compatibilité anciennes valeurs (conversations) : urgent→haute (rouge), normal→moyenne
+ urgent: { icon: 'flag', color: 'red-6', label: 'Haute', rank: 3 },
+ normal: { icon: 'outlined_flag', color: 'grey-5', label: 'Moyenne', rank: 2 },
}
-export const PRIORITY_LEVELS = ['urgent', 'high', 'normal', 'low'] // ordre d'affichage du menu (plus urgent → moins)
+export const PRIORITY_LEVELS = ['high', 'medium', 'low'] // 3 niveaux (standard ERPNext) — haute en tête
export function priorityMeta (p) {
return PRIORITIES[p] || { icon: 'outlined_flag', color: 'grey-5', label: 'Aucune', rank: 0 }
}
diff --git a/apps/ops/src/pages/PlanificationPage.vue b/apps/ops/src/pages/PlanificationPage.vue
index 6919a3d..4c3e549 100644
--- a/apps/ops/src/pages/PlanificationPage.vue
+++ b/apps/ops/src/pages/PlanificationPage.vue
@@ -1088,8 +1088,6 @@
{{ priorityMeta(lvl).label }}
-
- Aucune
Avant-midi (8-12) — Ré-optimiser pour appliquerAM
@@ -3930,14 +3928,12 @@ const suggestDlg = reactive({ open: false, mode: 'config', plan: [], building: f
const AMPM_WIN = { am: [480, 720], pm: [720, 960] } // 8-12 / 12-16 (heure d'horloge)
function jobTimeOf (name) { return suggestDlg.jobTime[name] || {} }
function toggleJobAmpm (name, v) { const c = suggestDlg.jobTime[name] || {}; suggestDlg.jobTime[name] = { ...c, ampm: c.ampm === v ? null : v } }
-// PRIORITÉ de dispatch PERSISTANTE par job — MÊME composant que les conversations (priorityMeta : drapeau contour gris → coloré + liste de niveaux). SLA/commercial.
+// 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) { const p = poolPriority.value[name] || ''; return (p === 'medium' || p === 'moyenne') ? '' : p } // medium ERPNext = non classé → drapeau contour gris (invite à classer)
-async function setEntryPriority (name, lvl) { // priorité de DISPATCH (store override, supporte urgent que le champ ERPNext low/medium/high ne fait pas)
- const j = (assignPanel.jobs || []).find(x => x.name === name); const prev = j ? j.priority : undefined
- if (j) j.priority = lvl || 'medium' // optimiste
- try { await roster.setJobFlag(name, lvl) } catch (e) { if (j) j.priority = prev; err(e); return }
- $q.notify({ type: 'info', icon: priorityMeta(lvl).icon, message: 'Priorité : ' + priorityMeta(lvl).label, timeout: 1600 })
+function jobPriorityOf (name) { return poolPriority.value[name] || '' } // medium → priorityMeta = contour gris (non classé)
+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)
}
// Réglages de l'optimiseur VRP (curseur distance↔spécialité, vitesse repli, budget de calcul) — persistés localement, ajustables sans redéploiement.
const solverOpts = reactive({ rankWeight: 2, speedKmh: 45, maxSeconds: 8 })
@@ -4149,8 +4145,8 @@ async function optimizeSuggestion () {
const selTechs = (visibleTechs.value || []).filter(t => suggestDlg.techSel[t.id])
// Priorité du job → « priority_boost » (pénalité de drop) : URGENT jamais laissé de côté ; BASSE lâchée en 1er si la capacité manque.
const poolPr = Object.fromEntries((assignPanel.jobs || []).map(j => [j.name, String(j.priority || '').toLowerCase()]))
- const prBoost = p => ({ urgent: 400000, high: 400000, 'élevée': 400000, medium: 0, moyenne: 0, normal: 0, low: -60000, basse: -60000 }[p] ?? 0)
- const prEarly = p => ({ urgent: 10, high: 4, 'élevée': 4 }[p] || 0) // priorité → poids « servir tôt »
+ const prBoost = p => ({ high: 400000, 'élevée': 400000, urgent: 400000, medium: 0, moyenne: 0, normal: 0, low: -60000, basse: -60000 }[p] ?? 0) // HAUTE jamais lâchée ; basse lâchée en 1er
+ const prEarly = p => ({ high: 10, 'élevée': 10, urgent: 10 }[p] || 0) // HAUTE = servie tôt (début de journée)
const out = []
for (const iso of Object.keys(byDay)) {
const entries = byDay[iso]
diff --git a/services/targo-hub/lib/conversation.js b/services/targo-hub/lib/conversation.js
index 3f76dc6..46e7a59 100644
--- a/services/targo-hub/lib/conversation.js
+++ b/services/targo-hub/lib/conversation.js
@@ -1811,7 +1811,7 @@ async function handle (req, res, method, p, url) {
if (sub === 'priority' && method === 'POST') {
const conv = getConversation(token); if (!conv) return json(res, 404, { error: 'Conversation not found' })
const b = await parseBody(req)
- const p = ['urgent', 'high', 'normal', 'low', ''].includes(b.priority) ? b.priority : ''
+ const p = ['high', 'medium', 'low', 'urgent', 'normal', ''].includes(b.priority) ? b.priority : '' // 3 niveaux (high/medium/low) ; urgent/normal gardés pour rétro-compat
conv.priority = p; saveToDisk()
const payload = { token: conv.token, priority: p }
sse.broadcast('conv:' + conv.token, 'conv-update', payload)
diff --git a/services/targo-hub/lib/roster.js b/services/targo-hub/lib/roster.js
index 3e2084f..a49bee1 100644
--- a/services/targo-hub/lib/roster.js
+++ b/services/targo-hub/lib/roster.js
@@ -45,17 +45,7 @@ function setJobLevel (name, level) {
try { fs.mkdirSync(path.dirname(JOB_LEVELS_FILE), { recursive: true }) } catch (e) {}
fs.writeFileSync(JOB_LEVELS_FILE, JSON.stringify(m)); return m
}
-// PRIORITÉ de dispatch persistante par job (même notion que la priorité des conversations : urgent|high|normal|low).
-// Store durable = SURCHARGE de la priorité ERPNext pour le dispatch (client commercial/SLA/contrainte).
-const JOB_FLAGS_FILE = path.join(__dirname, '..', 'data', 'job-flags.json')
-function getJobFlags () { try { return JSON.parse(fs.readFileSync(JOB_FLAGS_FILE, 'utf8')) || {} } catch { return {} } }
-const PRIO_LEVELS = ['urgent', 'high', 'normal', 'low']
-function setJobFlag (name, priority) {
- const m = getJobFlags(); const p = String(priority || '').toLowerCase()
- if (PRIO_LEVELS.includes(p)) m[String(name)] = p; else delete m[String(name)] // niveau valide → surcharge ; sinon (vide/« aucune ») → retire
- try { fs.mkdirSync(path.dirname(JOB_FLAGS_FILE), { recursive: true }) } catch (e) {}
- fs.writeFileSync(JOB_FLAGS_FILE, JSON.stringify(m)); return m
-}
+// Priorité de job = champ ERPNext standard (low/medium/high), édité via /roster/job/update (comme le pool). Plus de store parallèle.
const SOLVER_URL = cfg.ROSTER_SOLVER_URL || 'http://roster-solver:8090'
const PAUSE_STATUS = 'En pause'
@@ -456,8 +446,8 @@ async function buildUnassigned () {
const rows = await erp.list('Dispatch Job', { filters: [['status', 'in', ['open', 'On Hold']]], fields: ['name', 'creation', 'subject', 'customer_name', 'service_location', 'service_type', 'job_type', 'assigned_group', 'legacy_dept', 'legacy_detail', 'legacy_ticket_id', 'legacy_activation_url', 'priority', 'duration_h', 'scheduled_date', 'status', 'depends_on', 'parent_job', 'step_order', 'assigned_tech', 'latitude', 'longitude', 'address'], orderBy: 'modified desc', limit: 400 })
const jobs = (rows || []).filter(j => !j.assigned_tech)
const chars = readJobChar().items
- const jlv = getJobLevels(); const jfl = getJobFlags()
- for (const j of jobs) { j.required_skill = skillForJob(j) || deptToSkill(j.legacy_dept || j.job_type || j.subject); j.required_level = jlv[j.name] || 0; if (jfl[j.name]) j.priority = jfl[j.name]; const est = estimateForJob(j, chars); j.est_min = est.minutes; j.est_labels = est.labels }
+ const jlv = getJobLevels()
+ for (const j of jobs) { j.required_skill = skillForJob(j) || deptToSkill(j.legacy_dept || j.job_type || j.subject); j.required_level = jlv[j.name] || 0; const est = estimateForJob(j, chars); j.est_min = est.minutes; j.est_labels = est.labels }
await attachLocations(jobs)
log(`pool: construit ${jobs.length} jobs en ${nowMs() - t0}ms`) // mesure réelle (requête + enrichissement)
return jobs
@@ -1214,12 +1204,6 @@ async function handle (req, res, method, path, url) {
setJobLevel(b.name, b.level); invalidatePool() // le pool porte required_level → rafraîchir
return json(res, 200, { ok: true, name: b.name, level: Math.max(0, Math.min(5, Math.round(Number(b.level) || 0))) })
}
- if (path === '/roster/job-flag' && method === 'POST') { // PRIORITÉ de dispatch persistante par job (SLA/commercial) → store hub durable (surcharge)
- const b = await parseBody(req); if (!b.name) return json(res, 400, { error: 'name requis' })
- const p = String(b.priority || '').toLowerCase()
- setJobFlag(b.name, p); invalidatePool() // le pool porte j.priority → rafraîchir
- return json(res, 200, { ok: true, name: b.name, priority: PRIO_LEVELS.includes(p) ? p : '' })
- }
// Persister UN quart immédiatement (ex. « Créer un quart et assigner » sur un tech sans quart) — sinon `addShift` reste LOCAL
// (status Proposé en mémoire) et le quart disparaît au rechargement. Idempotent par clé tech|date|template (pas de doublon vs Publier).
if (path === '/roster/shift' && method === 'POST') {