Feature: add Paging & Intercom groups (Téléavertissement) with auto-answer headers, Asterisk Page application, and interactive member selection

This commit is contained in:
sorcier committed 2026-11-02 17:01:02 +01:00
1 parent 33ad75d571
commit 2bd1ae0777
5 files changed
+610

No files matched your search

+210
View File
@@ -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();