Feature: add Paging & Intercom groups (Téléavertissement) with auto-answer headers, Asterisk Page application, and interactive member selection
This commit is contained in:
1 parent
33ad75d571
commit
2bd1ae0777
5 files changed
+610
No files matched your search
@@ -4,6 +4,7 @@ let appState = {
|
||||
settings: {},
|
||||
extensions: [],
|
||||
groups: [],
|
||||
paging_groups: [],
|
||||
extension_groups: [],
|
||||
trunks: [],
|
||||
ivrs: [],
|
||||
@@ -200,6 +201,7 @@ function renderAll() {
|
||||
renderExtensionsTable();
|
||||
renderGroupsTable();
|
||||
renderExtensionGroupsTable();
|
||||
renderPagingTable();
|
||||
renderPhonebookTable();
|
||||
renderTrunksTable();
|
||||
renderOutboundRoutesTable();
|
||||
@@ -341,6 +343,8 @@ function renderSummaryCards() {
|
||||
document.getElementById('nav-badge-groups').textContent = grps.length;
|
||||
const egBadge = document.getElementById('nav-badge-extgroups');
|
||||
if (egBadge) egBadge.textContent = (appState.extension_groups || []).length;
|
||||
const pagingBadge = document.getElementById('nav-badge-paging');
|
||||
if (pagingBadge) pagingBadge.textContent = (appState.paging_groups || []).length;
|
||||
const pbBadge = document.getElementById('nav-badge-phonebook');
|
||||
if (pbBadge) pbBadge.textContent = (appState.phonebook_contacts || []).length;
|
||||
document.getElementById('nav-badge-trunks').textContent = trks.length;
|
||||
@@ -1379,6 +1383,178 @@ async function deleteExtGroup(id, optName = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2C. Rendu & CRUD Groupes de Téléavertissement & Interphonie (Paging) ---
|
||||
function renderPagingTable() {
|
||||
const tbody = document.getElementById('paging-tbody');
|
||||
if (!tbody) return;
|
||||
const rows = appState.paging_groups || [];
|
||||
|
||||
if (rows.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="8" class="py-8 text-center text-slate-400 text-xs">Aucun groupe de téléavertissement configuré. Cliquez sur « + Nouveau Groupe de Téléavertissement ».</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = rows.map(pg => {
|
||||
const members = Array.isArray(pg.members) ? pg.members : [];
|
||||
const badges = members.length
|
||||
? members.map(m => `<span class="px-2 py-0.5 rounded bg-purple-500/20 text-purple-200 border border-purple-500/30 font-mono text-xs font-semibold">${escapeHtml(m)}</span>`).join(' ')
|
||||
: '<span class="text-slate-500 text-xs italic">Aucun membre</span>';
|
||||
|
||||
const isDuplex = Number(pg.duplex) === 1;
|
||||
const typeBadge = isDuplex
|
||||
? `<span class="px-2 py-0.5 rounded text-[11px] font-semibold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">Duplex (2 voies)</span>`
|
||||
: `<span class="px-2 py-0.5 rounded text-[11px] font-semibold bg-purple-500/20 text-purple-300 border border-purple-500/30">Simplex (Haut-parleur)</span>`;
|
||||
|
||||
const busyLabel = pg.busy_mode === 'force'
|
||||
? `<span class="text-rose-300 text-xs font-semibold">Forcer</span>`
|
||||
: `<span class="text-emerald-300 text-xs font-semibold">Ignorer si occupé</span>`;
|
||||
|
||||
const beepLabel = Number(pg.play_beep) === 1
|
||||
? `<span class="text-emerald-400 text-xs font-semibold">✓ Oui</span>`
|
||||
: `<span class="text-slate-400 text-xs">Non</span>`;
|
||||
|
||||
const statusBadge = Number(pg.enabled) === 1
|
||||
? `<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">Actif</span>`
|
||||
: `<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-700 text-slate-300 border border-slate-600">Inactif</span>`;
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-anthracite-750/60 transition">
|
||||
<td class="py-3.5 px-4 font-mono font-bold text-purple-300 text-base">${escapeHtml(pg.extension)}</td>
|
||||
<td class="py-3.5 px-4">
|
||||
<div class="font-bold text-white">${escapeHtml(pg.name)}</div>
|
||||
${pg.description ? `<div class="text-xs text-slate-400">${escapeHtml(pg.description)}</div>` : ''}
|
||||
</td>
|
||||
<td class="py-3.5 px-4">${typeBadge}</td>
|
||||
<td class="py-3.5 px-4"><div class="flex flex-wrap gap-1">${badges}</div></td>
|
||||
<td class="py-3.5 px-4">${busyLabel}</td>
|
||||
<td class="py-3.5 px-4">${beepLabel}</td>
|
||||
<td class="py-3.5 px-4">${statusBadge}</td>
|
||||
<td class="py-3.5 px-4 text-right space-x-2">
|
||||
<button type="button" onclick="editPagingGroup(${Number(pg.id)})" class="text-xs font-semibold text-purple-300 hover:text-purple-200">Modifier</button>
|
||||
<button type="button" onclick="deletePagingGroup(${Number(pg.id)})" class="text-xs font-semibold text-red-400 hover:text-red-300">Supprimer</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
let activePagingSelectedExts = new Set();
|
||||
|
||||
function openPagingModal(pg = null) {
|
||||
const form = document.getElementById('form-paging');
|
||||
if (!form) return;
|
||||
form.reset();
|
||||
document.getElementById('modal-paging-title').textContent = pg
|
||||
? `Modifier le téléavertissement ${pg.name} (${pg.extension})`
|
||||
: "Nouveau Groupe de Téléavertissement";
|
||||
|
||||
form.elements['id'].value = pg ? pg.id : '';
|
||||
form.elements['extension'].value = pg ? pg.extension : '';
|
||||
form.elements['name'].value = pg ? pg.name : '';
|
||||
form.elements['description'].value = pg ? (pg.description || '') : '';
|
||||
form.elements['duplex'].value = pg ? String(pg.duplex) : '0';
|
||||
form.elements['busy_mode'].value = pg ? (pg.busy_mode || 'skip') : 'skip';
|
||||
form.elements['timeout'].value = pg ? (pg.timeout || 120) : 120;
|
||||
if (form.elements['play_beep']) {
|
||||
form.elements['play_beep'].checked = pg ? Number(pg.play_beep) === 1 : true;
|
||||
}
|
||||
if (form.elements['enabled']) {
|
||||
form.elements['enabled'].checked = pg ? Number(pg.enabled) === 1 : true;
|
||||
}
|
||||
|
||||
activePagingSelectedExts = new Set(pg && Array.isArray(pg.members) ? pg.members.map(String) : []);
|
||||
renderPagingInteractiveBoxes();
|
||||
|
||||
openModal('modal-paging');
|
||||
}
|
||||
|
||||
function renderPagingInteractiveBoxes() {
|
||||
const allowedBox = document.getElementById('paging-members-allowed-box');
|
||||
const blockedBox = document.getElementById('paging-members-blocked-box');
|
||||
const hiddenBox = document.getElementById('paging-members-checkboxes');
|
||||
if (!allowedBox || !blockedBox) return;
|
||||
|
||||
const allExts = appState.extensions || [];
|
||||
let allowedHtml = '';
|
||||
let blockedHtml = '';
|
||||
let hiddenHtml = '';
|
||||
let allowedCount = 0;
|
||||
let blockedCount = 0;
|
||||
|
||||
allExts.forEach(ext => {
|
||||
const extStr = String(ext.extension);
|
||||
const isSel = activePagingSelectedExts.has(extStr);
|
||||
if (isSel) {
|
||||
allowedCount++;
|
||||
allowedHtml += `
|
||||
<button type="button" onclick="togglePagingMemberExt('${escapeHtml(extStr)}')"
|
||||
class="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-purple-500/20 hover:bg-red-500/20 border border-purple-500/40 hover:border-red-500/40 text-xs font-semibold text-purple-300 hover:text-red-300 transition cursor-pointer">
|
||||
<span class="font-mono font-bold">${escapeHtml(extStr)}</span>
|
||||
<span class="text-slate-300">(${escapeHtml(ext.name)})</span>
|
||||
<span class="text-slate-400 group-hover:text-red-300 ml-1 font-bold">✕</span>
|
||||
</button>
|
||||
`;
|
||||
hiddenHtml += `<input type="checkbox" name="members[]" value="${escapeHtml(extStr)}" checked>`;
|
||||
} else {
|
||||
blockedCount++;
|
||||
blockedHtml += `
|
||||
<button type="button" onclick="togglePagingMemberExt('${escapeHtml(extStr)}')"
|
||||
class="group inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-anthracite-950 hover:bg-purple-500/20 border border-anthracite-700 hover:border-purple-500/40 text-xs font-medium text-slate-300 hover:text-purple-300 transition cursor-pointer">
|
||||
<span class="font-mono text-purple-300 font-bold">${escapeHtml(extStr)}</span>
|
||||
<span class="text-slate-300">(${escapeHtml(ext.name)})</span>
|
||||
<span class="text-slate-500 group-hover:text-purple-300 ml-1 font-bold">+</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
allowedBox.innerHTML = allowedHtml || `<div class="text-slate-500 text-xs italic py-2">Aucun poste qui reçoit l'annonce. Cliquez sur un poste à droite pour l'ajouter.</div>`;
|
||||
blockedBox.innerHTML = blockedHtml || `<div class="text-slate-500 text-xs italic py-2">Tous les postes reçoivent l'annonce !</div>`;
|
||||
if (hiddenBox) hiddenBox.innerHTML = hiddenHtml;
|
||||
|
||||
const countA = document.getElementById('paging-selected-count');
|
||||
const countB = document.getElementById('paging-unselected-count');
|
||||
if (countA) countA.textContent = String(allowedCount);
|
||||
if (countB) countB.textContent = String(blockedCount);
|
||||
}
|
||||
|
||||
function togglePagingMemberExt(extStr) {
|
||||
if (activePagingSelectedExts.has(extStr)) {
|
||||
activePagingSelectedExts.delete(extStr);
|
||||
} else {
|
||||
activePagingSelectedExts.add(extStr);
|
||||
}
|
||||
renderPagingInteractiveBoxes();
|
||||
}
|
||||
|
||||
function pagingTransferAllExtensions(includeAll) {
|
||||
const allExts = appState.extensions || [];
|
||||
if (includeAll) {
|
||||
allExts.forEach(e => activePagingSelectedExts.add(String(e.extension)));
|
||||
} else {
|
||||
activePagingSelectedExts.clear();
|
||||
}
|
||||
renderPagingInteractiveBoxes();
|
||||
}
|
||||
|
||||
function editPagingGroup(id) {
|
||||
const pg = (appState.paging_groups || []).find(g => Number(g.id) === Number(id));
|
||||
if (pg) openPagingModal(pg);
|
||||
}
|
||||
|
||||
async function deletePagingGroup(id) {
|
||||
const pg = (appState.paging_groups || []).find(g => Number(g.id) === Number(id));
|
||||
const num = pg ? pg.extension : 'ce groupe';
|
||||
if (!confirm(`Supprimer le groupe de téléavertissement ${num} ?`)) return;
|
||||
try {
|
||||
const res = await apiCall('delete_paging_group', { id: Number(id) });
|
||||
showToast(res.message, 'success');
|
||||
await loadState();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Rendu & CRUD Trunks SIP (Configuration PJSIP Avancée) ---
|
||||
function slugifyTrunkName(text) {
|
||||
return String(text || '')
|
||||
@@ -1512,6 +1688,7 @@ function toggleTrunkAuthFields(mode) {
|
||||
function normalizeDestinationValue(rawVal) {
|
||||
const exts = appState.extensions || [];
|
||||
const grps = appState.groups || [];
|
||||
const paging = appState.paging_groups || [];
|
||||
let normVal = String(rawVal || '').trim();
|
||||
if (!normVal) return 'ext-101';
|
||||
if (normVal === 'busy') return 'terminate-busy';
|
||||
@@ -1521,6 +1698,7 @@ function normalizeDestinationValue(rawVal) {
|
||||
if (/^\d+$/.test(normVal)) {
|
||||
if (exts.some(e => String(e.extension) === normVal)) return `ext-${normVal}`;
|
||||
if (grps.some(g => String(g.extension) === normVal)) return `grp-${normVal}`;
|
||||
if (paging.some(p => String(p.extension) === normVal)) return `page-${normVal}`;
|
||||
}
|
||||
return normVal;
|
||||
}
|
||||
@@ -1528,6 +1706,7 @@ function normalizeDestinationValue(rawVal) {
|
||||
function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
|
||||
const exts = appState.extensions || [];
|
||||
const grps = appState.groups || [];
|
||||
const paging = appState.paging_groups || [];
|
||||
const ivrs = appState.ivrs || [];
|
||||
const sounds = appState.sounds || [];
|
||||
const normVal = normalizeDestinationValue(selectedValue);
|
||||
@@ -1551,6 +1730,15 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
|
||||
html += `</optgroup>`;
|
||||
}
|
||||
|
||||
if (paging.length > 0) {
|
||||
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>`;
|
||||
});
|
||||
html += `</optgroup>`;
|
||||
}
|
||||
|
||||
if (ivrs.length > 0) {
|
||||
html += `<optgroup label="🎙️ Serveurs Vocaux (SVI / IVR)">`;
|
||||
ivrs.forEach(i => {
|
||||
@@ -3131,6 +3319,7 @@ const PAGE_TITLES = {
|
||||
extensions: 'Postes Téléphoniques SIP (PJSIP)',
|
||||
groups: "Groupes de Sonnerie (Ring Groups)",
|
||||
extgroups: "Groupes d'Extensions (Sites & Droits Sortants)",
|
||||
paging: 'Groupes de Téléavertissement & Interphonie (Paging)',
|
||||
trunks: 'Trunks Opérateurs SIP',
|
||||
outbound: 'Routes Sortantes (Outbound Routes & Séquence Trunks)',
|
||||
ivrs: 'Serveurs Vocaux Interactifs (SVI / IVR)',
|
||||
@@ -3366,6 +3555,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Form Groupe de Téléavertissement (Paging)
|
||||
document.getElementById('form-paging')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
const payload = Object.fromEntries(fd.entries());
|
||||
payload.members = Array.from(activePagingSelectedExts);
|
||||
payload.duplex = Number(payload.duplex || 0);
|
||||
payload.timeout = Number(payload.timeout || 120);
|
||||
payload.play_beep = e.target.querySelector('input[name="play_beep"]')?.checked ? 1 : 0;
|
||||
payload.enabled = e.target.querySelector('input[name="enabled"]')?.checked ? 1 : 0;
|
||||
|
||||
try {
|
||||
const res = await apiCall('save_paging_group', payload);
|
||||
showToast(res.message, 'success');
|
||||
closeModal('modal-paging');
|
||||
await loadState();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Form Trunk (Configuration PJSIP complète)
|
||||
document.getElementById('form-trunk')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in new issue
Block a user