feat(ring-groups): allow direct voicemail redirection on no-answer failover
Add vm-<ext> destination support in UI destination picker, API validation, and [app-voicemail] Asterisk dialplan context. Refs #2
This commit is contained in:
1 parent
3ef7ed0010
commit
d229aaf816
3 files changed
+64
-1
No files matched your search
@@ -544,6 +544,14 @@ try {
|
|||||||
if ($name === '') {
|
if ($name === '') {
|
||||||
throw new InvalidArgumentException("Le nom du groupe d'appel est obligatoire.");
|
throw new InvalidArgumentException("Le nom du groupe d'appel est obligatoire.");
|
||||||
}
|
}
|
||||||
|
if (str_starts_with($failover, 'vm-')) {
|
||||||
|
$vmTarget = substr($failover, 3);
|
||||||
|
$vmStmt = $db->prepare('SELECT voicemail_enabled FROM extensions WHERE extension = :ext AND enabled = 1');
|
||||||
|
$vmStmt->execute([':ext' => $vmTarget]);
|
||||||
|
if (!(int)$vmStmt->fetchColumn()) {
|
||||||
|
throw new InvalidArgumentException("Le répondeur du poste {$vmTarget} n'existe pas ou n'est pas activé.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($id) {
|
if ($id) {
|
||||||
$stmt = $db->prepare('
|
$stmt = $db->prepare('
|
||||||
|
|||||||
+21
-1
@@ -1234,6 +1234,9 @@ function renderGroupsTable() {
|
|||||||
const badges = members.length
|
const badges = members.length
|
||||||
? members.map(m => `<span class="px-2 py-0.5 rounded bg-indigo-500/20 text-indigo-200 border border-indigo-500/30 font-mono text-xs font-semibold">${escapeHtml(m)}</span>`).join(' ')
|
? members.map(m => `<span class="px-2 py-0.5 rounded bg-indigo-500/20 text-indigo-200 border border-indigo-500/30 font-mono text-xs font-semibold">${escapeHtml(m)}</span>`).join(' ')
|
||||||
: '<span class="text-slate-500 text-xs italic">Aucun membre</span>';
|
: '<span class="text-slate-500 text-xs italic">Aucun membre</span>';
|
||||||
|
const failoverLabel = String(grp.failover_dest || '').startsWith('vm-')
|
||||||
|
? `<span class="px-2 py-0.5 rounded bg-violet-500/20 text-violet-200 border border-violet-500/30 font-mono text-xs font-semibold">📬 Répondeur ${escapeHtml(String(grp.failover_dest).slice(3))}</span>`
|
||||||
|
: `<code class="text-slate-300">${escapeHtml(grp.failover_dest)}</code>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr class="hover:bg-anthracite-750/60 transition">
|
<tr class="hover:bg-anthracite-750/60 transition">
|
||||||
@@ -1241,7 +1244,7 @@ function renderGroupsTable() {
|
|||||||
<td class="py-3.5 px-4 font-bold text-white">${escapeHtml(grp.name)}</td>
|
<td class="py-3.5 px-4 font-bold text-white">${escapeHtml(grp.name)}</td>
|
||||||
<td class="py-3.5 px-4"><code class="text-xs text-cyan-300 font-mono">${escapeHtml(grp.strategy)}</code></td>
|
<td class="py-3.5 px-4"><code class="text-xs text-cyan-300 font-mono">${escapeHtml(grp.strategy)}</code></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"><div class="flex flex-wrap gap-1">${badges}</div></td>
|
||||||
<td class="py-3.5 px-4 text-xs text-slate-200">${Number(grp.ring_time)}s → <code class="text-slate-300">${escapeHtml(grp.failover_dest)}</code></td>
|
<td class="py-3.5 px-4 text-xs text-slate-200">${Number(grp.ring_time)}s → ${failoverLabel}</td>
|
||||||
<td class="py-3.5 px-4 text-right space-x-2">
|
<td class="py-3.5 px-4 text-right space-x-2">
|
||||||
<button type="button" onclick="editGroup(${Number(grp.id)})" class="text-xs font-semibold text-indigo-300 hover:text-indigo-200">Modifier</button>
|
<button type="button" onclick="editGroup(${Number(grp.id)})" class="text-xs font-semibold text-indigo-300 hover:text-indigo-200">Modifier</button>
|
||||||
<button type="button" onclick="deleteGroup(${Number(grp.id)})" class="text-xs font-semibold text-red-400 hover:text-red-300">Supprimer</button>
|
<button type="button" onclick="deleteGroup(${Number(grp.id)})" class="text-xs font-semibold text-red-400 hover:text-red-300">Supprimer</button>
|
||||||
@@ -1893,6 +1896,21 @@ function buildDestinationOptionsHtml(selectedValue = 'ext-101') {
|
|||||||
html += `</optgroup>`;
|
html += `</optgroup>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vmExts = exts.filter(e => Number(e.voicemail_enabled) === 1);
|
||||||
|
const hasMissingVm = normVal.startsWith('vm-') && !vmExts.some(e => `vm-${e.extension}` === normVal);
|
||||||
|
if (vmExts.length > 0 || hasMissingVm) {
|
||||||
|
html += `<optgroup label="📬 Messagerie Vocale Directe (Répondeur)">`;
|
||||||
|
if (hasMissingVm) {
|
||||||
|
const missingExt = normVal.slice(3);
|
||||||
|
html += `<option value="${escapeHtml(normVal)}" selected>Répondeur: ${escapeHtml(missingExt)} (messagerie désactivée)</option>`;
|
||||||
|
}
|
||||||
|
vmExts.forEach(e => {
|
||||||
|
const val = `vm-${e.extension}`;
|
||||||
|
html += `<option value="${escapeHtml(val)}" ${normVal === val ? 'selected' : ''}>Répondeur: ${escapeHtml(e.extension)} — ${escapeHtml(e.name)}</option>`;
|
||||||
|
});
|
||||||
|
html += `</optgroup>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Piège à Spammeurs / Musique d'attente en boucle continue (MoH)
|
// Piège à Spammeurs / Musique d'attente en boucle continue (MoH)
|
||||||
const mohSounds = sounds.filter(s => s.usage_type === 'moh');
|
const mohSounds = sounds.filter(s => s.usage_type === 'moh');
|
||||||
html += `<optgroup label="🎵 Piège à Spammeurs (Musique d'attente en boucle infinie)">`;
|
html += `<optgroup label="🎵 Piège à Spammeurs (Musique d'attente en boucle infinie)">`;
|
||||||
@@ -1948,6 +1966,8 @@ function styleDestinationSelect(selectEl) {
|
|||||||
selectEl.className = 'trunk-did-dest w-full px-2.5 py-1.5 rounded-lg bg-fuchsia-500/15 border border-fuchsia-500/40 text-xs text-fuchsia-300 font-semibold';
|
selectEl.className = 'trunk-did-dest w-full px-2.5 py-1.5 rounded-lg bg-fuchsia-500/15 border border-fuchsia-500/40 text-xs text-fuchsia-300 font-semibold';
|
||||||
} else if (val.startsWith('grp-')) {
|
} else if (val.startsWith('grp-')) {
|
||||||
selectEl.className = 'trunk-did-dest w-full px-2.5 py-1.5 rounded-lg bg-cyan-500/10 border border-cyan-500/40 text-xs text-cyan-300 font-semibold';
|
selectEl.className = 'trunk-did-dest w-full px-2.5 py-1.5 rounded-lg bg-cyan-500/10 border border-cyan-500/40 text-xs text-cyan-300 font-semibold';
|
||||||
|
} else if (val.startsWith('vm-')) {
|
||||||
|
selectEl.className = 'trunk-did-dest w-full px-2.5 py-1.5 rounded-lg bg-violet-500/15 border border-violet-500/40 text-xs text-violet-300 font-semibold';
|
||||||
} else {
|
} else {
|
||||||
selectEl.className = 'trunk-did-dest w-full px-2.5 py-1.5 rounded-lg bg-emerald-500/10 border border-emerald-500/40 text-xs text-emerald-300 font-semibold';
|
selectEl.className = 'trunk-did-dest w-full px-2.5 py-1.5 rounded-lg bg-emerald-500/10 border border-emerald-500/40 text-xs text-emerald-300 font-semibold';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1686,6 +1686,30 @@ class ConfigGenerator
|
|||||||
$lines[] = " same => n,Hangup()";
|
$lines[] = " same => n,Hangup()";
|
||||||
$lines[] = "";
|
$lines[] = "";
|
||||||
|
|
||||||
|
// 5b. Contexte dédié Dépôt Direct sur Boîte Vocale (Redirection sans sonnerie)
|
||||||
|
$lines[] = "; ===========================================================================";
|
||||||
|
$lines[] = "; DÉPÔT DIRECT SUR BOÎTE VOCALE (REDIRECTION / DÉBORDEMENT)";
|
||||||
|
$lines[] = "; ===========================================================================";
|
||||||
|
$lines[] = "[app-voicemail]";
|
||||||
|
foreach ($extensions as $ext) {
|
||||||
|
$num = trim((string)$ext['extension']);
|
||||||
|
if (empty($ext['voicemail_enabled'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$lines[] = "exten => {$num},1,NoOp(Redirection directe vers messagerie vocale du poste {$num})";
|
||||||
|
$lines[] = " same => n,Set(CHANNEL(language)={$sysLang})";
|
||||||
|
$lines[] = " same => n,Answer()";
|
||||||
|
$lines[] = " same => n,Wait(0.5)";
|
||||||
|
$lines[] = " same => n,VoiceMail({$num}@default,u)";
|
||||||
|
$lines[] = " same => n,Hangup()";
|
||||||
|
}
|
||||||
|
$lines[] = "exten => _X.,1,NoOp(Boite vocale inexistante ou desactivee pour \${EXTEN})";
|
||||||
|
$lines[] = " same => n,Set(CHANNEL(language)={$sysLang})";
|
||||||
|
$lines[] = " same => n,Answer()";
|
||||||
|
$lines[] = " same => n,Playback(vm-incorrect-mailbox)";
|
||||||
|
$lines[] = " same => n,Hangup()";
|
||||||
|
$lines[] = "";
|
||||||
|
|
||||||
return implode("\n", $lines) . "\n";
|
return implode("\n", $lines) . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2328,6 +2352,17 @@ EOF;
|
|||||||
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, 'vm-')) {
|
||||||
|
$vmExt = preg_replace('/[^0-9A-Za-z_-]/', '', substr($dest, 3));
|
||||||
|
if ($vmExt !== '') {
|
||||||
|
$stmt = $this->db->prepare('SELECT voicemail_enabled FROM extensions WHERE extension = :ext AND enabled = 1');
|
||||||
|
$stmt->execute([':ext' => $vmExt]);
|
||||||
|
if ($stmt->fetchColumn()) {
|
||||||
|
return "Goto(app-voicemail,{$vmExt},1)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'Goto(app-terminate,hangup,1)';
|
||||||
|
}
|
||||||
if (str_starts_with($dest, 'page-')) {
|
if (str_starts_with($dest, 'page-')) {
|
||||||
$pageExt = substr($dest, 5);
|
$pageExt = substr($dest, 5);
|
||||||
return "Goto(from-internal,{$pageExt},1)";
|
return "Goto(from-internal,{$pageExt},1)";
|
||||||
|
|||||||
Reference in new issue
Block a user