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();
|
||||
$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();
|
||||
$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();
|
||||
$trunks = $db->query('SELECT * FROM trunks ORDER BY id ASC')->fetchAll();
|
||||
$ivrs = $db->query('SELECT * FROM ivrs ORDER BY id ASC')->fetchAll();
|
||||
@@ -102,6 +103,11 @@ try {
|
||||
}
|
||||
unset($g);
|
||||
|
||||
foreach ($pagingGroups as &$pg) {
|
||||
$pg['members'] = json_decode((string)$pg['members'], true) ?: [];
|
||||
}
|
||||
unset($pg);
|
||||
|
||||
foreach ($extensionGroups as &$eg) {
|
||||
$eg['members'] = json_decode((string)$eg['members'], true) ?: [];
|
||||
}
|
||||
@@ -136,6 +142,7 @@ try {
|
||||
'settings' => $settings,
|
||||
'extensions' => $extensions,
|
||||
'groups' => $groups,
|
||||
'paging_groups' => $pagingGroups,
|
||||
'extension_groups' => $extensionGroups,
|
||||
'trunks' => $trunks,
|
||||
'ivrs' => $ivrs,
|
||||
@@ -441,6 +448,106 @@ try {
|
||||
]);
|
||||
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':
|
||||
$id = isset($payload['id']) && $payload['id'] !== '' ? (int)$payload['id'] : null;
|
||||
$name = trim((string)($payload['name'] ?? ''));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -289,6 +289,11 @@ $currentUser = Auth::getUsername();
|
||||
<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>
|
||||
</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')"
|
||||
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>
|
||||
@@ -1140,6 +1145,65 @@ $currentUser = Auth::getUsername();
|
||||
</div>
|
||||
</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 -->
|
||||
<!-- ========================================================= -->
|
||||
@@ -2813,6 +2877,106 @@ $currentUser = Auth::getUsername();
|
||||
</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) -->
|
||||
<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">
|
||||
|
||||
@@ -481,6 +481,7 @@ class ConfigGenerator
|
||||
$sysLang = trim((string)($settings['system_language'] ?? 'fr')) ?: 'fr';
|
||||
$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();
|
||||
$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();
|
||||
$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();
|
||||
@@ -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
|
||||
if (!empty($ivrs)) {
|
||||
$lines[] = "; --- Test direct des SVI/IVR depuis l'interne ---";
|
||||
@@ -1159,6 +1214,19 @@ class ConfigGenerator
|
||||
$lines[] = " same => n,Hangup()";
|
||||
$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)
|
||||
$lines[] = "; --- Pièges à Scammers & Démarchage (Tarpit Anti-Spam) ---";
|
||||
$lines[] = "[app-scam-tarpit]";
|
||||
@@ -1908,6 +1976,10 @@ class ConfigGenerator
|
||||
if (preg_match('/^\d+$/', $dest)) {
|
||||
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-')) {
|
||||
$ext = substr($dest, 4);
|
||||
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_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;
|
||||
|
||||
$db->exec($schema);
|
||||
@@ -301,6 +317,7 @@ class Database
|
||||
self::migrateCertificatesTable($db);
|
||||
self::migrateFeatureCodesTable($db);
|
||||
self::migratePhonebookContactsTable($db);
|
||||
self::migratePagingGroupsTable($db);
|
||||
|
||||
// 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();
|
||||
@@ -908,5 +925,45 @@ class Database
|
||||
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