ui(admin): v2.6.6 selecteur de destinations personnalise avec icones SVG style menu

This commit is contained in:
sorcier committed 2026-11-12 10:15:24 +01:00
1 parent b8cf932651
commit 70a021ef63
2 files changed
+211 -7

No files matched your search

+209 -6
View File
@@ -1882,7 +1882,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
let html = '';
if (exts.length > 0) {
html += `<optgroup label="📱 Postes SIP (Extensions)">`;
html += `<optgroup label="Postes SIP (Extensions)">`;
exts.forEach(e => {
const val = `ext-${e.extension}`;
html += `<option value="${escapeHtml(val)}" ${normVal === val ? 'selected' : ''}>Poste: ${escapeHtml(e.extension)} — ${escapeHtml(e.name)}</option>`;
@@ -1891,7 +1891,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
}
if (grps.length > 0) {
html += `<optgroup label="👥 Ring Groups (Groupes d'appel)">`;
html += `<optgroup label="Ring Groups (Groupes d'appel)">`;
grps.forEach(g => {
const val = `grp-${g.extension}`;
html += `<option value="${escapeHtml(val)}" ${normVal === val ? 'selected' : ''}>Ring Group: ${escapeHtml(g.extension)} — ${escapeHtml(g.name)}</option>`;
@@ -1900,7 +1900,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
}
if (paging.length > 0) {
html += `<optgroup label="📢 Téléavertissement & Interphonie (Paging)">`;
html += `<optgroup label="Téléavertissement & Interphonie (Paging)">`;
paging.forEach(pg => {
const val = `page-${pg.extension}`;
html += `<option value="${escapeHtml(val)}" ${normVal === val ? 'selected' : ''}>Paging: ${escapeHtml(pg.extension)} — ${escapeHtml(pg.name)}</option>`;
@@ -1909,7 +1909,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
}
if (ivrs.length > 0) {
html += `<optgroup label="🎙️ Serveurs Vocaux (SVI / IVR)">`;
html += `<optgroup label="Serveurs Vocaux (SVI / IVR)">`;
ivrs.forEach(i => {
const val = i.name;
html += `<option value="${escapeHtml(val)}" ${normVal === val ? 'selected' : ''}>SVI / IVR: ${escapeHtml(i.name)} ${i.description ? '(' + escapeHtml(i.description) + ')' : ''}</option>`;
@@ -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 += `<optgroup label="📬 Messagerie Vocale Directe (Répondeur)">`;
html += `<optgroup label="Messagerie Vocale Directe (Répondeur)">`;
if (hasMissingVm) {
const missingExt = normVal.slice(3);
html += `<option value="${escapeHtml(normVal)}" selected>Répondeur: ${escapeHtml(missingExt)} (messagerie désactivée)</option>`;
@@ -1960,7 +1960,7 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
}
html += `
<optgroup label="🚫 Terminer l'appel (Terminate Call)">
<optgroup label="Terminer l'appel (Terminate Call)">
<option value="terminate-hangup" ${normVal === 'terminate-hangup' || normVal === 'hangup' ? 'selected' : ''}>Raccrocher immédiatement (Hangup)</option>
<option value="terminate-busy" ${normVal === 'terminate-busy' || normVal === 'busy' ? 'selected' : ''}>Tonalité d'occupation (Busy)</option>
<option value="terminate-congestion" ${normVal === 'terminate-congestion' || normVal === 'congestion' ? 'selected' : ''}>Tonalité de congestion / réseau saturé (Congestion)</option>
@@ -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) => `<svg class="w-4 h-4 shrink-0 ${cls}" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24">${path}</svg>`;
if (v.startsWith('ext-') || l.includes('postes sip')) {
return mk('text-indigo-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z"/>');
}
if (v.startsWith('grp-') || l.includes('ring groups')) {
return mk('text-cyan-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"/>');
}
if (v.startsWith('page-') || l.includes('paging') || l.includes('interphonie')) {
return mk('text-purple-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"/>');
}
if (v.startsWith('vm-') || l.includes('messagerie') || l.includes('répondeur')) {
return mk('text-amber-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 9v.906a2.25 2.25 0 01-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 001.183 1.981l6.478 3.488m8.839 2.51l-4.66-2.51m0 0l-1.023-.55a2.25 2.25 0 00-2.134 0l-1.022.55m0 0l-4.661 2.51m16.5 1.615a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V8.844a2.25 2.25 0 011.183-1.98l7.5-4.04a2.25 2.25 0 012.134 0l7.5 4.04a2.25 2.25 0 011.183 1.98V19.5z"/>');
}
if (v.startsWith('moh-') || l.includes('musique')) {
return mk('text-sky-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M9 9l10.5-3m0 6.553v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 11-.99-3.467l2.31-.66a2.25 2.25 0 001.632-2.163zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 01-1.632 2.163l-1.32.377a1.803 1.803 0 01-.99-3.467l2.31-.66A2.25 2.25 0 009 15.553z"/>');
}
if (v.startsWith('sound-loop-') || l.includes('boucle continue')) {
return mk('text-violet-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"/>');
}
if (v.startsWith('sound-') || l.includes('annonces vocales')) {
return mk('text-teal-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"/>');
}
if (v.startsWith('terminate-') || ['hangup', 'busy', 'congestion', 'zapateller', 'ringforever'].includes(v) || l.includes('terminer')) {
return mk('text-rose-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>');
}
return mk('text-emerald-400', '<path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"/>');
}
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 = `
<span class="flex items-center gap-2 min-w-0">
${getDestinationIconSvg(val)}
<span class="truncate">${escapeHtml(txt)}</span>
</span>
<svg class="w-4 h-4 shrink-0 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/>
</svg>
`;
}
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 = `
<span class="flex items-center gap-2 min-w-0">
${getDestinationIconSvg(opt.value)}
<span class="truncate">${escapeHtml(opt.textContent)}</span>
</span>
${isSel ? '<svg class="w-3.5 h-3.5 shrink-0 text-indigo-400" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>' : ''}
`;
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)}<span>${escapeHtml(node.label)}</span>`;
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');