From 70a021ef63ef60e5724b01ea410971e438348acc Mon Sep 17 00:00:00 2001 From: sorcier Date: Thu, 12 Nov 2026 10:15:24 +0100 Subject: [PATCH] ui(admin): v2.6.6 selecteur de destinations personnalise avec icones SVG style menu --- public/assets/app.js | 215 +++++++++++++++++++++++++++++++++++++++++-- version.json | 3 +- 2 files changed, 211 insertions(+), 7 deletions(-) diff --git a/public/assets/app.js b/public/assets/app.js index 101789f..3b90078 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -1882,7 +1882,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') { let html = ''; if (exts.length > 0) { - html += ``; + html += ``; exts.forEach(e => { const val = `ext-${e.extension}`; html += ``; @@ -1891,7 +1891,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') { } if (grps.length > 0) { - html += ``; + html += ``; grps.forEach(g => { const val = `grp-${g.extension}`; html += ``; @@ -1900,7 +1900,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') { } if (paging.length > 0) { - html += ``; + html += ``; paging.forEach(pg => { const val = `page-${pg.extension}`; html += ``; @@ -1909,7 +1909,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') { } if (ivrs.length > 0) { - html += ``; + html += ``; ivrs.forEach(i => { const val = i.name; html += ``; @@ -1920,7 +1920,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') { const vmExts = exts.filter(e => Number(e.voicemail_enabled) === 1); const hasMissingVm = normVal.startsWith('vm-') && !vmExts.some(e => `vm-${e.extension}` === normVal); if (vmExts.length > 0 || hasMissingVm) { - html += ``; + html += ``; if (hasMissingVm) { const missingExt = normVal.slice(3); html += ``; @@ -1960,7 +1960,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') { } html += ` - + @@ -1972,6 +1972,209 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') { return html; } +// ----------------------------------------------------------------------------- +// SÉLECTEUR DE DESTINATION CUSTOM AVEC ICÔNES SVG (STYLE MENU LATÉRAL) +// ----------------------------------------------------------------------------- +let activeDestDropdownMenu = null; +let activeDestDropdownSelect = null; + +function getDestinationIconSvg(val, label = '') { + const v = String(val || ''); + const l = String(label || '').toLowerCase(); + const mk = (cls, path) => `${path}`; + + if (v.startsWith('ext-') || l.includes('postes sip')) { + return mk('text-indigo-400', ''); + } + if (v.startsWith('grp-') || l.includes('ring groups')) { + return mk('text-cyan-400', ''); + } + if (v.startsWith('page-') || l.includes('paging') || l.includes('interphonie')) { + return mk('text-purple-400', ''); + } + if (v.startsWith('vm-') || l.includes('messagerie') || l.includes('répondeur')) { + return mk('text-amber-400', ''); + } + if (v.startsWith('moh-') || l.includes('musique')) { + return mk('text-sky-400', ''); + } + if (v.startsWith('sound-loop-') || l.includes('boucle continue')) { + return mk('text-violet-400', ''); + } + if (v.startsWith('sound-') || l.includes('annonces vocales')) { + return mk('text-teal-400', ''); + } + if (v.startsWith('terminate-') || ['hangup', 'busy', 'congestion', 'zapateller', 'ringforever'].includes(v) || l.includes('terminer')) { + return mk('text-rose-400', ''); + } + return mk('text-emerald-400', ''); +} + +function closeDestinationDropdown() { + if (activeDestDropdownMenu) { + activeDestDropdownMenu.remove(); + activeDestDropdownMenu = null; + activeDestDropdownSelect = null; + } +} + +function updateDestinationSelectTrigger(selectEl) { + const wrap = selectEl._nexusDestWrap; + if (!wrap) return; + const btn = wrap.querySelector('.nexus-dest-trigger'); + if (!btn) return; + const opt = selectEl.options[selectEl.selectedIndex] || selectEl.options[0]; + const val = opt ? opt.value : ''; + const txt = opt ? opt.textContent : 'Sélectionner…'; + btn.innerHTML = ` + + ${getDestinationIconSvg(val)} + ${escapeHtml(txt)} + + + + + `; +} + +function openDestinationDropdown(selectEl, triggerBtn) { + if (activeDestDropdownSelect === selectEl) { + closeDestinationDropdown(); + return; + } + closeDestinationDropdown(); + + const menu = document.createElement('div'); + menu.className = 'fixed z-[120] bg-anthracite-900 border border-anthracite-700 rounded-xl shadow-2xl shadow-black/70 overflow-hidden flex flex-col'; + + const listEl = document.createElement('div'); + listEl.className = 'overflow-y-auto p-1.5 space-y-0.5 max-h-72'; + + const curVal = selectEl.value; + const appendOptionBtn = (opt) => { + const isSel = opt.value === curVal; + const item = document.createElement('button'); + item.type = 'button'; + item.className = `w-full flex items-center justify-between gap-2 px-2.5 py-1.5 rounded-lg text-left text-xs transition ${ + isSel ? 'bg-indigo-600/25 text-white font-semibold' : 'text-slate-200 hover:bg-anthracite-800 hover:text-white' + }`; + item.innerHTML = ` + + ${getDestinationIconSvg(opt.value)} + ${escapeHtml(opt.textContent)} + + ${isSel ? '' : ''} + `; + item.addEventListener('click', (e) => { + e.stopPropagation(); + selectEl.value = opt.value; + updateDestinationSelectTrigger(selectEl); + selectEl.dispatchEvent(new Event('change', { bubbles: true })); + closeDestinationDropdown(); + }); + listEl.appendChild(item); + }; + + Array.from(selectEl.children).forEach(node => { + if (node.tagName === 'OPTGROUP') { + const hdr = document.createElement('div'); + hdr.className = 'flex items-center gap-1.5 px-2 pt-2 pb-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 select-none'; + hdr.innerHTML = `${getDestinationIconSvg('', node.label)}${escapeHtml(node.label)}`; + listEl.appendChild(hdr); + Array.from(node.children).forEach(opt => { + if (opt.tagName === 'OPTION') appendOptionBtn(opt); + }); + } else if (node.tagName === 'OPTION') { + appendOptionBtn(node); + } + }); + + menu.appendChild(listEl); + document.body.appendChild(menu); + + const rect = triggerBtn.getBoundingClientRect(); + const width = Math.max(rect.width, 280); + const spaceBelow = window.innerHeight - rect.bottom - 12; + const menuHeight = Math.min(296, menu.scrollHeight || 296); + const openUp = spaceBelow < 180 && rect.top > spaceBelow; + menu.style.width = `${Math.min(width, window.innerWidth - 16)}px`; + menu.style.left = `${Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))}px`; + if (openUp) { + menu.style.bottom = `${window.innerHeight - rect.top + 4}px`; + } else { + menu.style.top = `${rect.bottom + 4}px`; + } + + activeDestDropdownMenu = menu; + activeDestDropdownSelect = selectEl; +} + +function enhanceDestinationSelect(selectEl) { + if (!selectEl || selectEl.tagName !== 'SELECT') return; + if (!selectEl.querySelector('option[value="terminate-hangup"]')) return; + + if (!selectEl._nexusDestWrap) { + selectEl.classList.add('sr-only'); + selectEl.setAttribute('tabindex', '-1'); + + const wrap = document.createElement('div'); + wrap.className = 'relative w-full'; + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'nexus-dest-trigger w-full flex items-center justify-between gap-2 px-3 py-2 rounded-xl bg-anthracite-950 border border-anthracite-700 hover:border-indigo-500/60 focus:border-indigo-500 focus:outline-none text-xs text-white text-left transition'; + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + openDestinationDropdown(selectEl, btn); + }); + + selectEl.parentNode.insertBefore(wrap, selectEl); + wrap.appendChild(selectEl); + wrap.appendChild(btn); + selectEl._nexusDestWrap = wrap; + + selectEl.addEventListener('change', () => updateDestinationSelectTrigger(selectEl)); + const optObs = new MutationObserver(() => updateDestinationSelectTrigger(selectEl)); + optObs.observe(selectEl, { childList: true, subtree: true }); + } + updateDestinationSelectTrigger(selectEl); +} + +if (typeof document !== 'undefined' && !window._nexusDestSelectInit) { + window._nexusDestSelectInit = true; + document.addEventListener('click', (e) => { + if (activeDestDropdownMenu && !activeDestDropdownMenu.contains(e.target)) { + closeDestinationDropdown(); + } + }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeDestinationDropdown(); + }); + window.addEventListener('resize', closeDestinationDropdown); + document.addEventListener('scroll', (e) => { + if (activeDestDropdownMenu && !activeDestDropdownMenu.contains(e.target)) { + closeDestinationDropdown(); + } + }, true); + + const domObs = new MutationObserver((mutations) => { + for (const m of mutations) { + if (m.target && m.target.tagName === 'SELECT') { + enhanceDestinationSelect(m.target); + } + m.addedNodes?.forEach(n => { + if (n.nodeType !== 1) return; + if (n.tagName === 'SELECT') enhanceDestinationSelect(n); + else n.querySelectorAll?.('select').forEach(enhanceDestinationSelect); + }); + } + }); + document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('select').forEach(enhanceDestinationSelect); + domObs.observe(document.body, { childList: true, subtree: true }); + }); +} + function updateInboundRoutesBadge() { const count = document.querySelectorAll('#trunk-did-container .trunk-did-row').length; const badge = document.getElementById('trunk-inbound-rules-badge'); diff --git a/version.json b/version.json index 687e950..297dc13 100644 --- a/version.json +++ b/version.json @@ -1,9 +1,10 @@ { - "version": "2.6.5", + "version": "2.6.6", "codename": "Pulsar UCP", "release_date": "2026-11-12", "min_php_version": "8.2", "changelog": [ + "UI v2.6.6 : menu déroulant personnalisé sombre avec icônes SVG Heroicons pour toutes les destinations d'appel (sans emojis natifs)", "UI v2.6.5 : refonte des intitulés de destinations audio (Musique d'attente & Annonce vocale en boucle continue), nouveau wallpaper clair UCP et placeholder générique", "Correctif v2.6.4 : prise en charge multi-groupes dans l'annuaire UCP & Admin (un poste appartenant à plusieurs groupes apparaît bien dans chacun des onglets correspondants)", "UI v2.6.3 : onglets dynamiques par groupe (Tous, Général, Paris, New-York...) avec compteurs dans l'Annuaire UCP",