feat(extensions): add Redémarrer action with call detection and confirmation modal
This commit is contained in:
1 parent
1908af5d13
commit
7fd75fbff4
4 files changed
+257
-3
No files matched your search
@@ -432,6 +432,82 @@ try {
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'check_extension_call_status':
|
||||
$extension = trim((string)($payload['extension'] ?? ''));
|
||||
if ($extension === '') {
|
||||
throw new InvalidArgumentException("Extension requise.");
|
||||
}
|
||||
|
||||
$inCall = false;
|
||||
$callDetails = '';
|
||||
try {
|
||||
$ami = AmiClient::fromDatabase($db);
|
||||
$rawChannels = $ami->runCommand("core show channels concise");
|
||||
$channelLines = array_filter(explode("\n", trim($rawChannels)));
|
||||
foreach ($channelLines as $cline) {
|
||||
$parts = explode('!', trim($cline));
|
||||
$chanName = $parts[0] ?? '';
|
||||
if (preg_match('#^PJSIP/' . preg_quote($extension, '#') . '[-/]#i', $chanName)) {
|
||||
$inCall = true;
|
||||
$state = $parts[4] ?? 'Up';
|
||||
$callerId = $parts[7] ?? '';
|
||||
$duration = $parts[10] ?? '';
|
||||
$callDetails = "Canal : {$chanName}";
|
||||
if ($callerId !== '') {
|
||||
$callDetails .= " • Interlocuteur : {$callerId}";
|
||||
}
|
||||
if ($duration !== '') {
|
||||
$callDetails .= " • Durée : {$duration}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$inCall) {
|
||||
$rawHints = $ami->runCommand("core show hints {$extension}");
|
||||
if (preg_match('/' . preg_quote($extension, '/') . '@[^\n]*State:(InUse|Busy|Ringing)/i', $rawHints, $m)) {
|
||||
$inCall = true;
|
||||
$callDetails = "Ligne active : " . ($m[1] === 'Ringing' ? 'En sonnerie' : 'En communication');
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// AMI indisponible
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'extension' => $extension,
|
||||
'in_call' => $inCall,
|
||||
'call_details' => $callDetails,
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'reboot_extension':
|
||||
$extension = trim((string)($payload['extension'] ?? ''));
|
||||
if ($extension === '') {
|
||||
throw new InvalidArgumentException("Extension requise.");
|
||||
}
|
||||
|
||||
$stmt = $db->prepare('SELECT extension, name FROM extensions WHERE extension = :ext');
|
||||
$stmt->execute([':ext' => $extension]);
|
||||
$extRow = $stmt->fetch();
|
||||
if (!$extRow) {
|
||||
throw new InvalidArgumentException("Poste {$extension} introuvable.");
|
||||
}
|
||||
|
||||
try {
|
||||
$ami = AmiClient::fromDatabase($db);
|
||||
$cmdOutput = $ami->runCommand("pjsip send notify reboot endpoint " . escapeshellarg($extension));
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => "Ordre de redémarrage envoyé au poste {$extension} ({$extRow['name']}).",
|
||||
'details' => trim($cmdOutput),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
throw new RuntimeException("Erreur lors de l'envoi de la commande de redémarrage : " . $e->getMessage());
|
||||
}
|
||||
break;
|
||||
|
||||
case 'save_group':
|
||||
$id = isset($payload['id']) && $payload['id'] !== '' ? (int)$payload['id'] : null;
|
||||
$extension = trim((string)($payload['extension'] ?? ''));
|
||||
|
||||
+88
-3
@@ -951,9 +951,10 @@ function renderExtensionsTable() {
|
||||
<td class="py-3.5 px-4">
|
||||
<code class="px-2 py-0.5 rounded bg-anthracite-950 border border-anthracite-700 text-xs font-mono text-slate-300">${escapeHtml(ext.secret)}</code>
|
||||
</td>
|
||||
<td class="py-3.5 px-4 text-right space-x-2">
|
||||
<button type="button" onclick="editExtension(${Number(ext.id)})" class="text-xs font-semibold text-indigo-300 hover:text-indigo-200">Modifier</button>
|
||||
<button type="button" onclick="deleteExtension(${Number(ext.id)})" class="text-xs font-semibold text-red-400 hover:text-red-300">Supprimer</button>
|
||||
<td class="py-3.5 px-4 text-right space-x-2 whitespace-nowrap">
|
||||
<button type="button" onclick="promptRebootExtension(${Number(ext.id)})" class="text-xs font-semibold text-amber-400 hover:text-amber-300 transition">Redémarrer</button>
|
||||
<button type="button" onclick="editExtension(${Number(ext.id)})" class="text-xs font-semibold text-indigo-300 hover:text-indigo-200 transition">Modifier</button>
|
||||
<button type="button" onclick="deleteExtension(${Number(ext.id)})" class="text-xs font-semibold text-red-400 hover:text-red-300 transition">Supprimer</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -1109,6 +1110,90 @@ async function deleteExtension(id, optNum = null) {
|
||||
}
|
||||
}
|
||||
|
||||
let pendingRebootExtension = null;
|
||||
|
||||
async function promptRebootExtension(id) {
|
||||
const ext = (appState.extensions || []).find(e => Number(e.id) === Number(id));
|
||||
if (!ext) return;
|
||||
|
||||
pendingRebootExtension = ext;
|
||||
|
||||
document.getElementById('reboot-modal-ext-num').textContent = ext.extension;
|
||||
document.getElementById('reboot-modal-ext-name').textContent = ext.name || '';
|
||||
|
||||
const checkingEl = document.getElementById('reboot-modal-checking');
|
||||
const warningEl = document.getElementById('reboot-modal-call-warning');
|
||||
const normalEl = document.getElementById('reboot-modal-normal-info');
|
||||
const btnAction = document.getElementById('btn-confirm-reboot-action');
|
||||
const btnLabel = document.getElementById('reboot-confirm-btn-label');
|
||||
|
||||
checkingEl.classList.remove('hidden');
|
||||
warningEl.classList.add('hidden');
|
||||
normalEl.classList.add('hidden');
|
||||
|
||||
btnAction.disabled = true;
|
||||
btnAction.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-anthracite-700 text-slate-400 transition cursor-not-allowed flex items-center gap-2';
|
||||
btnLabel.textContent = 'Vérification en cours...';
|
||||
|
||||
openModal('modal-confirm-reboot-extension');
|
||||
|
||||
try {
|
||||
const res = await apiCall('check_extension_call_status', { extension: ext.extension });
|
||||
checkingEl.classList.add('hidden');
|
||||
btnAction.disabled = false;
|
||||
|
||||
if (res.in_call) {
|
||||
warningEl.classList.remove('hidden');
|
||||
normalEl.classList.add('hidden');
|
||||
|
||||
const detailsEl = document.getElementById('reboot-modal-call-details');
|
||||
if (detailsEl) {
|
||||
detailsEl.textContent = res.call_details || 'Communication active détectée sur ce combiné.';
|
||||
}
|
||||
|
||||
btnAction.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-rose-600 hover:bg-rose-500 text-white shadow-lg shadow-rose-600/30 transition flex items-center gap-2';
|
||||
btnLabel.textContent = "Forcer le redémarrage (Couper l'appel)";
|
||||
} else {
|
||||
warningEl.classList.add('hidden');
|
||||
normalEl.classList.remove('hidden');
|
||||
|
||||
btnAction.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-600/30 transition flex items-center gap-2';
|
||||
btnLabel.textContent = 'Confirmer le redémarrage';
|
||||
}
|
||||
} catch (err) {
|
||||
checkingEl.classList.add('hidden');
|
||||
normalEl.classList.remove('hidden');
|
||||
btnAction.disabled = false;
|
||||
btnAction.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-600/30 transition flex items-center gap-2';
|
||||
btnLabel.textContent = 'Confirmer le redémarrage';
|
||||
}
|
||||
}
|
||||
|
||||
function closeRebootModal() {
|
||||
pendingRebootExtension = null;
|
||||
closeModal('modal-confirm-reboot-extension');
|
||||
}
|
||||
|
||||
async function confirmRebootExtension() {
|
||||
if (!pendingRebootExtension) return;
|
||||
const ext = pendingRebootExtension;
|
||||
const btnAction = document.getElementById('btn-confirm-reboot-action');
|
||||
const btnLabel = document.getElementById('reboot-confirm-btn-label');
|
||||
|
||||
if (btnAction) btnAction.disabled = true;
|
||||
if (btnLabel) btnLabel.textContent = 'Envoi de l\'ordre...';
|
||||
|
||||
try {
|
||||
const res = await apiCall('reboot_extension', { extension: ext.extension });
|
||||
closeRebootModal();
|
||||
showToast(res.message, 'success');
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
if (btnAction) btnAction.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function syncEncryptionToggle(transport, currentVal = null) {
|
||||
const sel = document.getElementById('ext-encryption');
|
||||
if (!sel) return;
|
||||
|
||||
@@ -5367,6 +5367,90 @@ $isDevEnv = file_exists(__DIR__ . '/../.dev')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- MODAL : CONFIRMATION DE REDÉMARRAGE POSTE SIP -->
|
||||
<!-- ===================================================================== -->
|
||||
<div id="modal-confirm-reboot-extension" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/80 backdrop-blur-sm p-3 sm:p-4 overflow-y-auto">
|
||||
<div class="bg-anthracite-900 border border-amber-500/40 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden my-6 animate-fadeIn">
|
||||
<!-- Entête -->
|
||||
<div class="px-6 py-5 border-b border-anthracite-800 bg-gradient-to-r from-amber-950/40 via-anthracite-900 to-anthracite-900 flex items-start justify-between">
|
||||
<div class="flex items-center gap-3.5">
|
||||
<div class="w-11 h-11 rounded-xl bg-amber-500/15 border border-amber-500/30 text-amber-400 flex items-center justify-center shrink-0 shadow-lg shadow-amber-950/50">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-base font-bold text-white">Redémarrer le Téléphone IP</h3>
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-bold tracking-wide uppercase bg-amber-500/20 text-amber-300 border border-amber-500/30">SIP NOTIFY</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-0.5">
|
||||
Poste <span id="reboot-modal-ext-num" class="font-mono font-bold text-indigo-300"></span> — <span id="reboot-modal-ext-name" class="font-medium text-slate-200"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="closeRebootModal()" class="text-slate-400 hover:text-white p-1.5 rounded-lg hover:bg-anthracite-800 transition">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Corps -->
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- Indicateur de vérification de l'état en direct -->
|
||||
<div id="reboot-modal-checking" class="flex items-center gap-2.5 p-3 rounded-xl bg-anthracite-950 border border-anthracite-800 text-xs text-slate-400">
|
||||
<svg class="w-4 h-4 text-indigo-400 animate-spin shrink-0" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path></svg>
|
||||
<span>Vérification des appels en cours sur ce combiné...</span>
|
||||
</div>
|
||||
|
||||
<!-- ALERTE ROUGE : Appel / conversation en cours -->
|
||||
<div id="reboot-modal-call-warning" class="hidden p-4 rounded-xl bg-rose-500/15 border-2 border-rose-500/60 space-y-3">
|
||||
<div class="flex items-center gap-2 text-rose-400 font-bold text-sm">
|
||||
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
<span>ATTENTION : CONVERSATION EN COURS SUR CE POSTE !</span>
|
||||
</div>
|
||||
<p class="text-xs text-rose-200">
|
||||
Ce combiné est actuellement <strong class="underline">en communication active</strong>.
|
||||
</p>
|
||||
<div id="reboot-modal-call-details" class="p-2.5 rounded-lg bg-black/50 border border-rose-500/40 text-xs font-mono text-rose-300"></div>
|
||||
<div class="p-3 rounded-lg bg-rose-950/60 border border-rose-500/40 text-xs font-semibold text-rose-200 flex items-start gap-2">
|
||||
<span class="text-base leading-none">⚠️</span>
|
||||
<span>Si vous confirmez le redémarrage, <strong>la conversation téléphonique sera immédiatement coupée</strong> pour votre interlocuteur !</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Information normale (quand pas d'appel) -->
|
||||
<div id="reboot-modal-normal-info" class="hidden space-y-3">
|
||||
<div class="p-3.5 rounded-xl bg-anthracite-950 border border-anthracite-800 text-xs text-slate-300 space-y-2">
|
||||
<div class="flex items-center gap-2 text-emerald-400 font-semibold">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
||||
<span>Aucune conversation en cours (poste libre)</span>
|
||||
</div>
|
||||
<p class="text-slate-400">
|
||||
Un ordre SIP NOTIFY standard (<code class="text-amber-300 font-mono">check-sync;reboot=true</code>) va être envoyé au téléphone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3.5 rounded-xl bg-amber-500/10 border border-amber-500/20 text-xs text-amber-200 space-y-1">
|
||||
<div class="font-bold">Effet du redémarrage :</div>
|
||||
<p class="text-slate-300">Le téléphone va redémarrer, recharger son provisioning et synchroniser ses touches BLF (interruption d'environ 30 à 45 secondes).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boutons d'action -->
|
||||
<div class="px-6 py-4 border-t border-anthracite-800 bg-anthracite-950/80 flex items-center justify-end gap-3">
|
||||
<button type="button" onclick="closeRebootModal()"
|
||||
class="px-4 py-2.5 rounded-xl text-xs font-semibold bg-anthracite-800 hover:bg-anthracite-750 text-slate-300 border border-anthracite-700 transition">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="button" id="btn-confirm-reboot-action" onclick="confirmRebootExtension()"
|
||||
class="px-5 py-2.5 rounded-xl text-xs font-bold bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-600/30 transition flex items-center gap-2">
|
||||
<span id="reboot-confirm-btn-label">Confirmer le redémarrage</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/app.js?v=<?= filemtime(__DIR__ . '/assets/app.js') ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user