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">
|
||||
|
||||
Reference in new issue
Block a user