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
+107
@@ -90,6 +90,7 @@ try {
|
|||||||
$settings = $db->query('SELECT * FROM settings WHERE id = 1')->fetch();
|
$settings = $db->query('SELECT * FROM settings WHERE id = 1')->fetch();
|
||||||
$extensions = $db->query('SELECT * FROM extensions ORDER BY CAST(extension AS INTEGER) ASC, extension ASC')->fetchAll();
|
$extensions = $db->query('SELECT * FROM extensions ORDER BY CAST(extension AS INTEGER) ASC, extension ASC')->fetchAll();
|
||||||
$groups = $db->query('SELECT * FROM groups ORDER BY CAST(extension AS INTEGER) ASC, extension ASC')->fetchAll();
|
$groups = $db->query('SELECT * FROM groups ORDER BY CAST(extension AS INTEGER) ASC, extension ASC')->fetchAll();
|
||||||
|
$pagingGroups = $db->query('SELECT * FROM paging_groups ORDER BY CAST(extension AS INTEGER) ASC, extension ASC')->fetchAll();
|
||||||
$extensionGroups = $db->query('SELECT * FROM extension_groups ORDER BY id ASC')->fetchAll();
|
$extensionGroups = $db->query('SELECT * FROM extension_groups ORDER BY id ASC')->fetchAll();
|
||||||
$trunks = $db->query('SELECT * FROM trunks ORDER BY id ASC')->fetchAll();
|
$trunks = $db->query('SELECT * FROM trunks ORDER BY id ASC')->fetchAll();
|
||||||
$ivrs = $db->query('SELECT * FROM ivrs ORDER BY id ASC')->fetchAll();
|
$ivrs = $db->query('SELECT * FROM ivrs ORDER BY id ASC')->fetchAll();
|
||||||
@@ -102,6 +103,11 @@ try {
|
|||||||
}
|
}
|
||||||
unset($g);
|
unset($g);
|
||||||
|
|
||||||
|
foreach ($pagingGroups as &$pg) {
|
||||||
|
$pg['members'] = json_decode((string)$pg['members'], true) ?: [];
|
||||||
|
}
|
||||||
|
unset($pg);
|
||||||
|
|
||||||
foreach ($extensionGroups as &$eg) {
|
foreach ($extensionGroups as &$eg) {
|
||||||
$eg['members'] = json_decode((string)$eg['members'], true) ?: [];
|
$eg['members'] = json_decode((string)$eg['members'], true) ?: [];
|
||||||
}
|
}
|
||||||
@@ -136,6 +142,7 @@ try {
|
|||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'extensions' => $extensions,
|
'extensions' => $extensions,
|
||||||
'groups' => $groups,
|
'groups' => $groups,
|
||||||
|
'paging_groups' => $pagingGroups,
|
||||||
'extension_groups' => $extensionGroups,
|
'extension_groups' => $extensionGroups,
|
||||||
'trunks' => $trunks,
|
'trunks' => $trunks,
|
||||||
'ivrs' => $ivrs,
|
'ivrs' => $ivrs,
|
||||||
@@ -441,6 +448,106 @@ try {
|
|||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'save_paging_group':
|
||||||
|
$id = isset($payload['id']) && $payload['id'] !== '' ? (int)$payload['id'] : null;
|
||||||
|
$extension = trim((string)($payload['extension'] ?? ''));
|
||||||
|
$name = trim((string)($payload['name'] ?? ''));
|
||||||
|
$desc = trim((string)($payload['description'] ?? ''));
|
||||||
|
$members = is_array($payload['members'] ?? null) ? array_values(array_filter(array_map('trim', $payload['members']))) : [];
|
||||||
|
$duplex = !empty($payload['duplex']) ? 1 : 0;
|
||||||
|
$busyMode = in_array($payload['busy_mode'] ?? '', ['skip', 'force'], true) ? $payload['busy_mode'] : 'skip';
|
||||||
|
$playBeep = isset($payload['play_beep']) ? (!empty($payload['play_beep']) ? 1 : 0) : 1;
|
||||||
|
$timeout = max(10, min(600, (int)($payload['timeout'] ?? 120)));
|
||||||
|
$enabled = isset($payload['enabled']) ? (!empty($payload['enabled']) ? 1 : 0) : 1;
|
||||||
|
|
||||||
|
if (!preg_match('/^\d{2,6}$/', $extension)) {
|
||||||
|
throw new InvalidArgumentException("Le numéro du groupe de téléavertissement doit comporter entre 2 et 6 chiffres.");
|
||||||
|
}
|
||||||
|
if ($name === '') {
|
||||||
|
throw new InvalidArgumentException("Le nom du groupe de téléavertissement est obligatoire.");
|
||||||
|
}
|
||||||
|
if (empty($members)) {
|
||||||
|
throw new InvalidArgumentException("Veuillez sélectionner au moins un poste membre pour ce groupe de téléavertissement.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification de conflit avec un poste SIP existant
|
||||||
|
$chkExt = $db->prepare('SELECT COUNT(*) FROM extensions WHERE extension = :ext');
|
||||||
|
$chkExt->execute([':ext' => $extension]);
|
||||||
|
if ((int)$chkExt->fetchColumn() > 0) {
|
||||||
|
throw new InvalidArgumentException("Le numéro {$extension} est déjà assigné à un poste SIP individuel.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification de conflit avec un groupe de sonnerie
|
||||||
|
$chkGrp = $db->prepare('SELECT COUNT(*) FROM groups WHERE extension = :ext');
|
||||||
|
$chkGrp->execute([':ext' => $extension]);
|
||||||
|
if ((int)$chkGrp->fetchColumn() > 0) {
|
||||||
|
throw new InvalidArgumentException("Le numéro {$extension} est déjà assigné à un groupe de sonnerie.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
UPDATE paging_groups
|
||||||
|
SET extension = :ext, name = :name, description = :desc,
|
||||||
|
members = :members, duplex = :duplex, busy_mode = :busy_mode,
|
||||||
|
play_beep = :play_beep, timeout = :timeout, enabled = :enabled,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = :id
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
':id' => $id,
|
||||||
|
':ext' => $extension,
|
||||||
|
':name' => $name,
|
||||||
|
':desc' => $desc,
|
||||||
|
':members' => json_encode($members),
|
||||||
|
':duplex' => $duplex,
|
||||||
|
':busy_mode' => $busyMode,
|
||||||
|
':play_beep' => $playBeep,
|
||||||
|
':timeout' => $timeout,
|
||||||
|
':enabled' => $enabled,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
INSERT INTO paging_groups (extension, name, description, members, duplex, busy_mode, play_beep, timeout, enabled)
|
||||||
|
VALUES (:ext, :name, :desc, :members, :duplex, :busy_mode, :play_beep, :timeout, :enabled)
|
||||||
|
ON CONFLICT(extension) DO UPDATE SET
|
||||||
|
name = excluded.name,
|
||||||
|
description = excluded.description,
|
||||||
|
members = excluded.members,
|
||||||
|
duplex = excluded.duplex,
|
||||||
|
busy_mode = excluded.busy_mode,
|
||||||
|
play_beep = excluded.play_beep,
|
||||||
|
timeout = excluded.timeout,
|
||||||
|
enabled = excluded.enabled,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
':ext' => $extension,
|
||||||
|
':name' => $name,
|
||||||
|
':desc' => $desc,
|
||||||
|
':members' => json_encode($members),
|
||||||
|
':duplex' => $duplex,
|
||||||
|
':busy_mode' => $busyMode,
|
||||||
|
':play_beep' => $playBeep,
|
||||||
|
':timeout' => $timeout,
|
||||||
|
':enabled' => $enabled,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => "Groupe de téléavertissement {$extension} ({$name}) sauvegardé.",
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete_paging_group':
|
||||||
|
$id = (int)($payload['id'] ?? 0);
|
||||||
|
$db->prepare('DELETE FROM paging_groups WHERE id = :id')->execute([':id' => $id]);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => "Groupe de téléavertissement supprimé.",
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'save_extension_group':
|
case 'save_extension_group':
|
||||||
$id = isset($payload['id']) && $payload['id'] !== '' ? (int)$payload['id'] : null;
|
$id = isset($payload['id']) && $payload['id'] !== '' ? (int)$payload['id'] : null;
|
||||||
$name = trim((string)($payload['name'] ?? ''));
|
$name = trim((string)($payload['name'] ?? ''));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ let appState = {
|
|||||||
settings: {},
|
settings: {},
|
||||||
extensions: [],
|
extensions: [],
|
||||||
groups: [],
|
groups: [],
|
||||||
|
paging_groups: [],
|
||||||
extension_groups: [],
|
extension_groups: [],
|
||||||
trunks: [],
|
trunks: [],
|
||||||
ivrs: [],
|
ivrs: [],
|
||||||
@@ -200,6 +201,7 @@ function renderAll() {
|
|||||||
renderExtensionsTable();
|
renderExtensionsTable();
|
||||||
renderGroupsTable();
|
renderGroupsTable();
|
||||||
renderExtensionGroupsTable();
|
renderExtensionGroupsTable();
|
||||||
|
renderPagingTable();
|
||||||
renderPhonebookTable();
|
renderPhonebookTable();
|
||||||
renderTrunksTable();
|
renderTrunksTable();
|
||||||
renderOutboundRoutesTable();
|
renderOutboundRoutesTable();
|
||||||
@@ -341,6 +343,8 @@ function renderSummaryCards() {
|
|||||||
document.getElementById('nav-badge-groups').textContent = grps.length;
|
document.getElementById('nav-badge-groups').textContent = grps.length;
|
||||||
const egBadge = document.getElementById('nav-badge-extgroups');
|
const egBadge = document.getElementById('nav-badge-extgroups');
|
||||||
if (egBadge) egBadge.textContent = (appState.extension_groups || []).length;
|
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');
|
const pbBadge = document.getElementById('nav-badge-phonebook');
|
||||||
if (pbBadge) pbBadge.textContent = (appState.phonebook_contacts || []).length;
|
if (pbBadge) pbBadge.textContent = (appState.phonebook_contacts || []).length;
|
||||||
document.getElementById('nav-badge-trunks').textContent = trks.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) ---
|
// --- 3. Rendu & CRUD Trunks SIP (Configuration PJSIP Avancée) ---
|
||||||
function slugifyTrunkName(text) {
|
function slugifyTrunkName(text) {
|
||||||
return String(text || '')
|
return String(text || '')
|
||||||
@@ -1512,6 +1688,7 @@ function toggleTrunkAuthFields(mode) {
|
|||||||
function normalizeDestinationValue(rawVal) {
|
function normalizeDestinationValue(rawVal) {
|
||||||
const exts = appState.extensions || [];
|
const exts = appState.extensions || [];
|
||||||
const grps = appState.groups || [];
|
const grps = appState.groups || [];
|
||||||
|
const paging = appState.paging_groups || [];
|
||||||
let normVal = String(rawVal || '').trim();
|
let normVal = String(rawVal || '').trim();
|
||||||
if (!normVal) return 'ext-101';
|
if (!normVal) return 'ext-101';
|
||||||
if (normVal === 'busy') return 'terminate-busy';
|
if (normVal === 'busy') return 'terminate-busy';
|
||||||
@@ -1521,6 +1698,7 @@ function normalizeDestinationValue(rawVal) {
|
|||||||
if (/^\d+$/.test(normVal)) {
|
if (/^\d+$/.test(normVal)) {
|
||||||
if (exts.some(e => String(e.extension) === normVal)) return `ext-${normVal}`;
|
if (exts.some(e => String(e.extension) === normVal)) return `ext-${normVal}`;
|
||||||
if (grps.some(g => String(g.extension) === normVal)) return `grp-${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;
|
return normVal;
|
||||||
}
|
}
|
||||||
@@ -1528,6 +1706,7 @@ function normalizeDestinationValue(rawVal) {
|
|||||||
function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
|
function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
|
||||||
const exts = appState.extensions || [];
|
const exts = appState.extensions || [];
|
||||||
const grps = appState.groups || [];
|
const grps = appState.groups || [];
|
||||||
|
const paging = appState.paging_groups || [];
|
||||||
const ivrs = appState.ivrs || [];
|
const ivrs = appState.ivrs || [];
|
||||||
const sounds = appState.sounds || [];
|
const sounds = appState.sounds || [];
|
||||||
const normVal = normalizeDestinationValue(selectedValue);
|
const normVal = normalizeDestinationValue(selectedValue);
|
||||||
@@ -1551,6 +1730,15 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
|
|||||||
html += `</optgroup>`;
|
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) {
|
if (ivrs.length > 0) {
|
||||||
html += `<optgroup label="🎙️ Serveurs Vocaux (SVI / IVR)">`;
|
html += `<optgroup label="🎙️ Serveurs Vocaux (SVI / IVR)">`;
|
||||||
ivrs.forEach(i => {
|
ivrs.forEach(i => {
|
||||||
@@ -3131,6 +3319,7 @@ const PAGE_TITLES = {
|
|||||||
extensions: 'Postes Téléphoniques SIP (PJSIP)',
|
extensions: 'Postes Téléphoniques SIP (PJSIP)',
|
||||||
groups: "Groupes de Sonnerie (Ring Groups)",
|
groups: "Groupes de Sonnerie (Ring Groups)",
|
||||||
extgroups: "Groupes d'Extensions (Sites & Droits Sortants)",
|
extgroups: "Groupes d'Extensions (Sites & Droits Sortants)",
|
||||||
|
paging: 'Groupes de Téléavertissement & Interphonie (Paging)',
|
||||||
trunks: 'Trunks Opérateurs SIP',
|
trunks: 'Trunks Opérateurs SIP',
|
||||||
outbound: 'Routes Sortantes (Outbound Routes & Séquence Trunks)',
|
outbound: 'Routes Sortantes (Outbound Routes & Séquence Trunks)',
|
||||||
ivrs: 'Serveurs Vocaux Interactifs (SVI / IVR)',
|
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)
|
// Form Trunk (Configuration PJSIP complète)
|
||||||
document.getElementById('form-trunk')?.addEventListener('submit', async (e) => {
|
document.getElementById('form-trunk')?.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -289,6 +289,11 @@ $currentUser = Auth::getUsername();
|
|||||||
<span>• Groupes d'extensions</span>
|
<span>• Groupes d'extensions</span>
|
||||||
<span id="nav-badge-extgroups" class="px-1.5 py-0.2 text-[10px] rounded-full bg-anthracite-800 text-cyan-300 border border-anthracite-700 font-bold">0</span>
|
<span id="nav-badge-extgroups" class="px-1.5 py-0.2 text-[10px] rounded-full bg-anthracite-800 text-cyan-300 border border-anthracite-700 font-bold">0</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" data-tab="paging" onclick="switchTab('paging')"
|
||||||
|
class="tab-btn w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-xs font-medium text-slate-300 hover:bg-anthracite-800 hover:text-white transition">
|
||||||
|
<span>• Téléavertissement (Paging)</span>
|
||||||
|
<span id="nav-badge-paging" class="px-1.5 py-0.2 text-[10px] rounded-full bg-anthracite-800 text-purple-300 border border-anthracite-700 font-bold">0</span>
|
||||||
|
</button>
|
||||||
<button type="button" data-tab="phonebook" onclick="switchTab('phonebook')"
|
<button type="button" data-tab="phonebook" onclick="switchTab('phonebook')"
|
||||||
class="tab-btn w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-xs font-medium text-slate-300 hover:bg-anthracite-800 hover:text-white transition">
|
class="tab-btn w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-xs font-medium text-slate-300 hover:bg-anthracite-800 hover:text-white transition">
|
||||||
<span>• Annuaire d'Entreprise</span>
|
<span>• Annuaire d'Entreprise</span>
|
||||||
@@ -1140,6 +1145,65 @@ $currentUser = Auth::getUsername();
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<!-- ONGLET 2D : TÉLÉAVERTISSEMENT & INTERPHONIE (PAGING) -->
|
||||||
|
<!-- ========================================================= -->
|
||||||
|
<section id="tab-paging" class="tab-panel hidden space-y-4">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h2 class="text-lg font-bold text-white">Groupes de Téléavertissement & Interphonie (Paging)</h2>
|
||||||
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold bg-purple-500/20 text-purple-300 border border-purple-500/30">Auto-décroché immédiat</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-300">Diffusion d'annonces vocales en direct sur les haut-parleurs des téléphones IP sélectionnés. Composez le numéro du groupe (ex: <code class="text-purple-300 font-mono font-bold">100</code>) pour parler à tous les postes simultanément.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" onclick="openPagingModal()"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs sm:text-sm font-semibold bg-purple-600 hover:bg-purple-500 text-white shadow-lg shadow-purple-600/20 transition">
|
||||||
|
<span>+ Nouveau Groupe de Téléavertissement</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bannière d'aide rapide Paging & Intercom 1-à-1 -->
|
||||||
|
<div class="p-4 rounded-2xl bg-anthracite-800/90 border border-purple-500/30 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="w-8 h-8 rounded-xl bg-purple-500/20 border border-purple-500/40 flex items-center justify-center text-purple-300 shrink-0 font-bold text-sm">📢</div>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<div class="text-xs font-bold text-purple-200">Annonce Générale de Groupe (Paging)</div>
|
||||||
|
<p class="text-[11px] text-slate-300">Composez le numéro du groupe configuré (ex: <span class="font-mono text-purple-300 font-bold">100</span>). Les téléphones cibles décrochent automatiquement sur haut-parleur après un bip d'avertissement sonore.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="w-8 h-8 rounded-xl bg-indigo-500/20 border border-indigo-500/40 flex items-center justify-center text-indigo-300 shrink-0 font-bold text-sm">📞</div>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<div class="text-xs font-bold text-indigo-200">Interphonie Directe 1-à-1 (*80 + poste)</div>
|
||||||
|
<p class="text-[11px] text-slate-300">Composez <span class="font-mono text-indigo-300 font-bold">*80</span> suivi du numéro de l'extension (ex: <span class="font-mono text-indigo-300 font-bold">*80101</span>) pour ouvrir directement le haut-parleur et le micro du poste en conversation duplex.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-anthracite-800 border border-anthracite-700 rounded-2xl overflow-hidden shadow-lg">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-anthracite-700 text-[11px] font-semibold uppercase tracking-wider text-slate-300 bg-anthracite-950/60">
|
||||||
|
<th class="py-3.5 px-4">Numéro (Extension)</th>
|
||||||
|
<th class="py-3.5 px-4">Nom du Groupe</th>
|
||||||
|
<th class="py-3.5 px-4">Mode Audio</th>
|
||||||
|
<th class="py-3.5 px-4">Postes Membres Associés</th>
|
||||||
|
<th class="py-3.5 px-4">Si Occupé</th>
|
||||||
|
<th class="py-3.5 px-4">Bip</th>
|
||||||
|
<th class="py-3.5 px-4">Statut</th>
|
||||||
|
<th class="py-3.5 px-4 text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="paging-tbody" class="divide-y divide-anthracite-700/70 text-sm"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ========================================================= -->
|
<!-- ========================================================= -->
|
||||||
<!-- ONGLET 3 : TRUNKS SIP -->
|
<!-- ONGLET 3 : TRUNKS SIP -->
|
||||||
<!-- ========================================================= -->
|
<!-- ========================================================= -->
|
||||||
@@ -2813,6 +2877,106 @@ $currentUser = Auth::getUsername();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- MODALE CRUD GROUPE DE TÉLÉAVERTISSEMENT & INTERPHONIE (PAGING) -->
|
||||||
|
<div id="modal-paging" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/75 backdrop-blur-sm p-3 sm:p-4 overflow-y-auto">
|
||||||
|
<div class="bg-anthracite-800 border border-anthracite-700 rounded-2xl w-[94vw] max-w-4xl p-6 shadow-2xl my-auto">
|
||||||
|
<div class="flex items-center justify-between pb-3 mb-4 border-b border-anthracite-700">
|
||||||
|
<div>
|
||||||
|
<h3 id="modal-paging-title" class="text-base sm:text-lg font-bold text-white">Groupe de Téléavertissement (Paging / Intercom)</h3>
|
||||||
|
<p class="text-xs text-slate-300">Diffusion d'annonces vocales directes avec auto-décroché immédiat sur haut-parleur.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="closeModal('modal-paging')" class="text-slate-300 hover:text-white text-lg">✕</button>
|
||||||
|
</div>
|
||||||
|
<form id="form-paging" class="space-y-4">
|
||||||
|
<input type="hidden" name="id">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-12 gap-4">
|
||||||
|
<div class="sm:col-span-3">
|
||||||
|
<label class="block text-xs font-medium text-slate-200 mb-1">Numéro d'appel (Extension) *</label>
|
||||||
|
<input type="text" name="extension" placeholder="ex: 100" required
|
||||||
|
class="w-full px-3 py-2 rounded-lg bg-anthracite-950 border border-anthracite-700 text-sm text-purple-300 font-mono font-bold">
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label class="block text-xs font-medium text-slate-200 mb-1">Nom du groupe *</label>
|
||||||
|
<input type="text" name="name" placeholder="ex: Téléavertissement Général, Atelier..." required
|
||||||
|
class="w-full px-3 py-2 rounded-lg bg-anthracite-950 border border-anthracite-700 text-sm text-white">
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-4">
|
||||||
|
<label class="block text-xs font-medium text-slate-200 mb-1">Description (optionnel)</label>
|
||||||
|
<input type="text" name="description" placeholder="ex: Annonce vocale globale"
|
||||||
|
class="w-full px-3 py-2 rounded-lg bg-anthracite-950 border border-anthracite-700 text-sm text-white">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-slate-200 mb-1">Mode Audio</label>
|
||||||
|
<select name="duplex" class="w-full px-3 py-2 rounded-lg bg-anthracite-950 border border-anthracite-700 text-sm text-white">
|
||||||
|
<option value="0">Simplex (Haut-parleur seul / Micro coupé)</option>
|
||||||
|
<option value="1">Duplex (Interphonie 2 voies / Conversation)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-slate-200 mb-1">Gestion des Postes Occupés</label>
|
||||||
|
<select name="busy_mode" class="w-full px-3 py-2 rounded-lg bg-anthracite-950 border border-anthracite-700 text-sm text-white">
|
||||||
|
<option value="skip">Ignorer si le poste est en communication</option>
|
||||||
|
<option value="force">Forcer l'annonce même si en ligne</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-slate-200 mb-1">Durée maximale d'annonce (sec)</label>
|
||||||
|
<input type="number" name="timeout" value="120" min="10" max="600" required
|
||||||
|
class="w-full px-3 py-2 rounded-lg bg-anthracite-950 border border-anthracite-700 text-sm text-white">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-6 p-3 rounded-xl bg-anthracite-950 border border-anthracite-700">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" name="play_beep" value="1" checked class="w-4 h-4 rounded text-purple-600 focus:ring-purple-500 bg-anthracite-900 border-anthracite-700">
|
||||||
|
<span class="text-xs font-medium text-slate-200">Émettre un bip sonore d'avertissement avant la prise de parole</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" name="enabled" value="1" checked class="w-4 h-4 rounded text-emerald-600 focus:ring-emerald-500 bg-anthracite-900 border-anthracite-700">
|
||||||
|
<span class="text-xs font-medium text-slate-200">Activer ce groupe de téléavertissement</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sélection Double Liste Interactive : Postes qui reçoivent l'annonce -->
|
||||||
|
<div class="p-4 rounded-xl bg-anthracite-950 border border-purple-500/40 space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-xs font-bold uppercase tracking-wider text-purple-300">Postes membres qui recevront l'annonce (Cliquez pour inclure / retirer)</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button type="button" onclick="pagingTransferAllExtensions(true)" class="px-2.5 py-1 rounded-lg bg-purple-500/15 hover:bg-purple-500/25 border border-purple-500/30 text-[11px] font-semibold text-purple-300">← Tout inclure</button>
|
||||||
|
<button type="button" onclick="pagingTransferAllExtensions(false)" class="px-2.5 py-1 rounded-lg bg-anthracite-800 hover:bg-anthracite-700 border border-anthracite-600 text-[11px] font-semibold text-slate-300">Tout retirer →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="p-3 rounded-xl bg-anthracite-900 border border-purple-500/30 min-h-[130px]">
|
||||||
|
<div class="text-xs font-bold text-purple-300 mb-2 flex items-center justify-between">
|
||||||
|
<span>✓ Postes ciblés par le téléavertissement</span>
|
||||||
|
<span id="paging-selected-count" class="px-2 py-0.2 rounded-full bg-purple-500/20 text-[10px]">0</span>
|
||||||
|
</div>
|
||||||
|
<div id="paging-members-allowed-box" class="flex flex-wrap gap-1.5"></div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 rounded-xl bg-anthracite-900 border border-anthracite-700 min-h-[130px]">
|
||||||
|
<div class="text-xs font-bold text-slate-400 mb-2 flex items-center justify-between">
|
||||||
|
<span>Autres postes disponibles</span>
|
||||||
|
<span id="paging-unselected-count" class="px-2 py-0.2 rounded-full bg-anthracite-800 text-[10px]">0</span>
|
||||||
|
</div>
|
||||||
|
<div id="paging-members-blocked-box" class="flex flex-wrap gap-1.5"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="paging-members-checkboxes" class="hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-3 border-t border-anthracite-700 flex justify-end gap-2">
|
||||||
|
<button type="button" onclick="closeModal('modal-paging')" class="px-4 py-2 rounded-xl bg-anthracite-700 text-slate-200 text-xs font-semibold">Annuler</button>
|
||||||
|
<button type="submit" class="px-4 py-2 rounded-xl bg-purple-600 hover:bg-purple-500 text-white text-xs font-semibold">Enregistrer le groupe de téléavertissement</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- MODALE CRUD ROUTE SORTANTE (OUTBOUND ROUTE : TRUNK SEQUENCE, DIAL PATTERNS, EXTENSION & GROUP ROUTING) -->
|
<!-- MODALE CRUD ROUTE SORTANTE (OUTBOUND ROUTE : TRUNK SEQUENCE, DIAL PATTERNS, EXTENSION & GROUP ROUTING) -->
|
||||||
<div id="modal-outbound" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/75 backdrop-blur-sm p-2 sm:p-4 overflow-y-auto">
|
<div id="modal-outbound" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/75 backdrop-blur-sm p-2 sm:p-4 overflow-y-auto">
|
||||||
<div class="bg-anthracite-800 border border-anthracite-700 rounded-2xl w-[96vw] max-w-[1380px] p-5 sm:p-7 shadow-2xl my-auto max-h-[94vh] flex flex-col">
|
<div class="bg-anthracite-800 border border-anthracite-700 rounded-2xl w-[96vw] max-w-[1380px] p-5 sm:p-7 shadow-2xl my-auto max-h-[94vh] flex flex-col">
|
||||||
|
|||||||
@@ -481,6 +481,7 @@ class ConfigGenerator
|
|||||||
$sysLang = trim((string)($settings['system_language'] ?? 'fr')) ?: 'fr';
|
$sysLang = trim((string)($settings['system_language'] ?? 'fr')) ?: 'fr';
|
||||||
$extensions = $this->db->query('SELECT * FROM extensions WHERE enabled = 1 ORDER BY extension ASC')->fetchAll();
|
$extensions = $this->db->query('SELECT * FROM extensions WHERE enabled = 1 ORDER BY extension ASC')->fetchAll();
|
||||||
$groups = $this->db->query('SELECT * FROM groups ORDER BY extension ASC')->fetchAll();
|
$groups = $this->db->query('SELECT * FROM groups ORDER BY extension ASC')->fetchAll();
|
||||||
|
$pagingGroups = $this->db->query('SELECT * FROM paging_groups WHERE enabled = 1 ORDER BY CAST(extension AS INTEGER) ASC')->fetchAll();
|
||||||
$extGroups = $this->db->query('SELECT * FROM extension_groups ORDER BY id ASC')->fetchAll();
|
$extGroups = $this->db->query('SELECT * FROM extension_groups ORDER BY id ASC')->fetchAll();
|
||||||
$trunks = $this->db->query('SELECT * FROM trunks WHERE enabled = 1 ORDER BY id ASC')->fetchAll();
|
$trunks = $this->db->query('SELECT * FROM trunks WHERE enabled = 1 ORDER BY id ASC')->fetchAll();
|
||||||
$ivrs = $this->db->query('SELECT * FROM ivrs ORDER BY name ASC')->fetchAll();
|
$ivrs = $this->db->query('SELECT * FROM ivrs ORDER BY name ASC')->fetchAll();
|
||||||
@@ -908,6 +909,60 @@ class ConfigGenerator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Groupes de téléavertissement & Interphonie (Paging)
|
||||||
|
if (!empty($pagingGroups)) {
|
||||||
|
$lines[] = "; --- Groupes de téléavertissement & Interphonie (Paging) ---";
|
||||||
|
foreach ($pagingGroups as $pg) {
|
||||||
|
$pExt = trim((string)$pg['extension']);
|
||||||
|
$pName = str_replace('"', '', trim((string)$pg['name']));
|
||||||
|
$pDuplex = !empty($pg['duplex']);
|
||||||
|
$pBusyMode = trim((string)($pg['busy_mode'] ?? 'skip'));
|
||||||
|
$pBeep = !empty($pg['play_beep']);
|
||||||
|
$pTimeout = max(10, min(600, (int)($pg['timeout'] ?? 120)));
|
||||||
|
$members = json_decode((string)$pg['members'], true) ?: [];
|
||||||
|
$members = array_values(array_filter(array_map('trim', $members)));
|
||||||
|
|
||||||
|
$lines[] = "exten => {$pExt},1,NoOp(Teleavertissement Groupe {$pExt} - {$pName})";
|
||||||
|
$lines[] = " same => n,Set(CHANNEL(language)={$sysLang})";
|
||||||
|
$lines[] = " same => n,Answer()";
|
||||||
|
if ($pBeep) {
|
||||||
|
$lines[] = " same => n,Playback(beep)";
|
||||||
|
}
|
||||||
|
if (empty($members)) {
|
||||||
|
$lines[] = " same => n,Playback(beeperr)";
|
||||||
|
$lines[] = " same => n,Hangup()";
|
||||||
|
} else {
|
||||||
|
$devStr = implode('&', array_map(fn($m) => "PJSIP/{$m}", $members));
|
||||||
|
// Options Page():
|
||||||
|
// b(sub-paging-call^s^1) : configure les en-têtes SIP d'auto-réponse (Auto-Answer) sur chaque branche
|
||||||
|
// q : muet (pas de musique de fond confbridge)
|
||||||
|
// d : duplex (interphonie bidirectionnelle) si activé, sinon simplex (haut-parleur seul)
|
||||||
|
// s : skip channels if state != NOT_INUSE (évite d'interrompre un appel en cours si busy_mode = skip)
|
||||||
|
$pageOpts = "b(sub-paging-call^s^1)q";
|
||||||
|
if ($pDuplex) {
|
||||||
|
$pageOpts .= "d";
|
||||||
|
}
|
||||||
|
if ($pBusyMode === 'skip') {
|
||||||
|
$pageOpts .= "s";
|
||||||
|
}
|
||||||
|
$lines[] = " same => n,Set(PAGE_DEVICES={$devStr})";
|
||||||
|
$lines[] = " same => n,Page(\${PAGE_DEVICES},{$pageOpts},{$pTimeout})";
|
||||||
|
$lines[] = " same => n,Hangup()";
|
||||||
|
}
|
||||||
|
$lines[] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interphonie directe 1-à-1 (*80 + numéro de poste)
|
||||||
|
$lines[] = "; --- Interphonie directe 1-à-1 (*80 + numéro de poste) ---";
|
||||||
|
$lines[] = "exten => _*80.,1,NoOp(Interphonie directe vers poste \${EXTEN:3})";
|
||||||
|
$lines[] = " same => n,Set(CHANNEL(language)={$sysLang})";
|
||||||
|
$lines[] = " same => n,Answer()";
|
||||||
|
$lines[] = " same => n,Playback(beep)";
|
||||||
|
$lines[] = " same => n,Page(PJSIP/\${EXTEN:3},b(sub-paging-call^s^1)qd,120)";
|
||||||
|
$lines[] = " same => n,Hangup()";
|
||||||
|
$lines[] = "";
|
||||||
|
}
|
||||||
|
|
||||||
// Accès direct aux IVR depuis l'interne via *8 + id
|
// Accès direct aux IVR depuis l'interne via *8 + id
|
||||||
if (!empty($ivrs)) {
|
if (!empty($ivrs)) {
|
||||||
$lines[] = "; --- Test direct des SVI/IVR depuis l'interne ---";
|
$lines[] = "; --- Test direct des SVI/IVR depuis l'interne ---";
|
||||||
@@ -1159,6 +1214,19 @@ class ConfigGenerator
|
|||||||
$lines[] = " same => n,Hangup()";
|
$lines[] = " same => n,Hangup()";
|
||||||
$lines[] = "";
|
$lines[] = "";
|
||||||
|
|
||||||
|
// 2a. Sous-routine d'interphonie / Auto-Answer Téléavertissement (Paging)
|
||||||
|
$lines[] = "; --- Sous-routine d'interphonie / Auto-Answer Téléavertissement ---";
|
||||||
|
$lines[] = "[sub-paging-call]";
|
||||||
|
$lines[] = "exten => s,1,NoOp(Preparation entetes SIP Auto-Answer pour Paging)";
|
||||||
|
$lines[] = " same => n,Set(PJSIP_HEADER(add,Alert-Info)=info=alert-autoanswer\\;delay=0)";
|
||||||
|
$lines[] = " same => n,Set(PJSIP_HEADER(add,Alert-Info)=<http://www.notused.com>\\;info=alert-autoanswer\\;delay=0)";
|
||||||
|
$lines[] = " same => n,Set(PJSIP_HEADER(add,Call-Info)=\\;answer-after=0)";
|
||||||
|
$lines[] = " same => n,Set(PJSIP_HEADER(add,P-Auto-Answer)=normal)";
|
||||||
|
$lines[] = " same => n,Set(PJSIP_HEADER(add,Answer-After)=0)";
|
||||||
|
$lines[] = " same => n,Set(PJSIP_HEADER(add,SIP-Auto-Answer)=true)";
|
||||||
|
$lines[] = " same => n,Return()";
|
||||||
|
$lines[] = "";
|
||||||
|
|
||||||
// 2a-bis. Contexte [app-scam-tarpit] (Pièges à Scammers : Musique d'attente en boucle & Fausse voix en boucle infinie)
|
// 2a-bis. Contexte [app-scam-tarpit] (Pièges à Scammers : Musique d'attente en boucle & Fausse voix en boucle infinie)
|
||||||
$lines[] = "; --- Pièges à Scammers & Démarchage (Tarpit Anti-Spam) ---";
|
$lines[] = "; --- Pièges à Scammers & Démarchage (Tarpit Anti-Spam) ---";
|
||||||
$lines[] = "[app-scam-tarpit]";
|
$lines[] = "[app-scam-tarpit]";
|
||||||
@@ -1908,6 +1976,10 @@ class ConfigGenerator
|
|||||||
if (preg_match('/^\d+$/', $dest)) {
|
if (preg_match('/^\d+$/', $dest)) {
|
||||||
return "Goto(from-internal,{$dest},1)";
|
return "Goto(from-internal,{$dest},1)";
|
||||||
}
|
}
|
||||||
|
if (str_starts_with($dest, 'page-')) {
|
||||||
|
$pageExt = substr($dest, 5);
|
||||||
|
return "Goto(from-internal,{$pageExt},1)";
|
||||||
|
}
|
||||||
if (str_starts_with($dest, 'ext-')) {
|
if (str_starts_with($dest, 'ext-')) {
|
||||||
$ext = substr($dest, 4);
|
$ext = substr($dest, 4);
|
||||||
return "Goto(from-internal,{$ext},1)";
|
return "Goto(from-internal,{$ext},1)";
|
||||||
|
|||||||
@@ -287,6 +287,22 @@ class Database
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_phonebook_ext ON phonebook_contacts(extension_id);
|
CREATE INDEX IF NOT EXISTS idx_phonebook_ext ON phonebook_contacts(extension_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_phonebook_group ON phonebook_contacts(group_name);
|
CREATE INDEX IF NOT EXISTS idx_phonebook_group ON phonebook_contacts(group_name);
|
||||||
|
|
||||||
|
-- 14. Table paging_groups : Groupes de téléavertissement & Interphonie (Paging)
|
||||||
|
CREATE TABLE IF NOT EXISTS paging_groups (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
extension VARCHAR(16) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(64) NOT NULL,
|
||||||
|
description VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
members TEXT NOT NULL DEFAULT '[]',
|
||||||
|
duplex INTEGER NOT NULL DEFAULT 0,
|
||||||
|
busy_mode VARCHAR(16) NOT NULL DEFAULT 'skip',
|
||||||
|
play_beep INTEGER NOT NULL DEFAULT 1,
|
||||||
|
timeout INTEGER NOT NULL DEFAULT 120,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
SQL;
|
SQL;
|
||||||
|
|
||||||
$db->exec($schema);
|
$db->exec($schema);
|
||||||
@@ -301,6 +317,7 @@ class Database
|
|||||||
self::migrateCertificatesTable($db);
|
self::migrateCertificatesTable($db);
|
||||||
self::migrateFeatureCodesTable($db);
|
self::migrateFeatureCodesTable($db);
|
||||||
self::migratePhonebookContactsTable($db);
|
self::migratePhonebookContactsTable($db);
|
||||||
|
self::migratePagingGroupsTable($db);
|
||||||
|
|
||||||
// Création du compte admin par défaut (admin / admin123) s'il n'existe aucun utilisateur
|
// Création du compte admin par défaut (admin / admin123) s'il n'existe aucun utilisateur
|
||||||
$userCount = (int)$db->query('SELECT COUNT(*) FROM users')->fetchColumn();
|
$userCount = (int)$db->query('SELECT COUNT(*) FROM users')->fetchColumn();
|
||||||
@@ -908,5 +925,45 @@ class Database
|
|||||||
CREATE INDEX IF NOT EXISTS idx_phonebook_group ON phonebook_contacts(group_name);
|
CREATE INDEX IF NOT EXISTS idx_phonebook_group ON phonebook_contacts(group_name);
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function migratePagingGroupsTable(PDO $db): void
|
||||||
|
{
|
||||||
|
$db->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS paging_groups (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
extension VARCHAR(16) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(64) NOT NULL,
|
||||||
|
description VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
members TEXT NOT NULL DEFAULT '[]',
|
||||||
|
duplex INTEGER NOT NULL DEFAULT 0,
|
||||||
|
busy_mode VARCHAR(16) NOT NULL DEFAULT 'skip',
|
||||||
|
play_beep INTEGER NOT NULL DEFAULT 1,
|
||||||
|
timeout INTEGER NOT NULL DEFAULT 120,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
");
|
||||||
|
|
||||||
|
$count = (int)$db->query('SELECT COUNT(*) FROM paging_groups')->fetchColumn();
|
||||||
|
if ($count === 0) {
|
||||||
|
$exts = $db->query('SELECT extension FROM extensions ORDER BY CAST(extension AS INTEGER) ASC')->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
$members = !empty($exts) ? $exts : ['101', '102'];
|
||||||
|
$stmt = $db->prepare('
|
||||||
|
INSERT INTO paging_groups (extension, name, description, members, duplex, busy_mode, play_beep, timeout, enabled)
|
||||||
|
VALUES (:ext, :name, :desc, :members, :duplex, :busy_mode, :play_beep, :timeout, 1)
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
':ext' => '100',
|
||||||
|
':name' => 'Téléavertissement Général',
|
||||||
|
':desc' => 'Diffusion générale sur haut-parleur de tous les postes',
|
||||||
|
':members' => json_encode($members),
|
||||||
|
':duplex' => 0,
|
||||||
|
':busy_mode' => 'skip',
|
||||||
|
':play_beep' => 1,
|
||||||
|
':timeout' => 120,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user