534 lines
16 KiB
Markdown
534 lines
16 KiB
Markdown
# Guide d'intégration SignalR pour SimpleConnect
|
|
|
|
## Vue d'ensemble
|
|
|
|
Ce document décrit l'architecture SignalR utilisée dans SimpleConnect v2 pour implémenter une intégration CTI (Computer Telephony Integration) complète avec gestion en temps réel des appels et basculement automatique entre les différentes plateformes de prise de rendez-vous.
|
|
|
|
## Architecture générale
|
|
|
|
### Composants principaux
|
|
|
|
1. **Serveur SignalR** : Hub centralisé gérant les événements téléphoniques
|
|
2. **Client Electron** : Application agent avec connexion persistante au hub
|
|
3. **Système IPBX** : Infrastructure téléphonique envoyant les événements
|
|
|
|
### Configuration serveur
|
|
|
|
```json
|
|
{
|
|
"ServerIp": "10.90.20.201:8002",
|
|
"ServiceProvider": "RDVPREM"
|
|
}
|
|
```
|
|
|
|
## Installation des dépendances
|
|
|
|
```bash
|
|
npm install @microsoft/signalr
|
|
```
|
|
|
|
## Implémentation du client SignalR
|
|
|
|
### 1. Connexion au Hub
|
|
|
|
```javascript
|
|
const signalR = require("@microsoft/signalr");
|
|
|
|
// Créer la connexion au hub SignalR
|
|
function createSignalRConnection(serverUrl) {
|
|
const connection = new signalR.HubConnectionBuilder()
|
|
.withUrl(`http://${serverUrl}/signalR`)
|
|
.withAutomaticReconnect([0, 2000, 5000, 10000]) // Reconnexion automatique
|
|
.configureLogging(signalR.LogLevel.Information)
|
|
.build();
|
|
|
|
return connection;
|
|
}
|
|
|
|
// Démarrer la connexion
|
|
async function startConnection(connection) {
|
|
try {
|
|
await connection.start();
|
|
console.log('Connexion SignalR établie');
|
|
return true;
|
|
} catch (err) {
|
|
console.error('Erreur de connexion SignalR:', err);
|
|
setTimeout(() => startConnection(connection), 5000);
|
|
return false;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Authentification des agents
|
|
|
|
```javascript
|
|
// Structure de connexion agent
|
|
async function agentLogin(connection, credentials) {
|
|
try {
|
|
const result = await connection.invoke('AgentLogin',
|
|
credentials.email,
|
|
credentials.password,
|
|
credentials.terminal
|
|
);
|
|
|
|
if (result) {
|
|
// Stockage des informations agent
|
|
global.AgentConnectionInfo = {
|
|
accessCode: result.accessCode,
|
|
firstName: result.firstName,
|
|
lastName: result.lastName,
|
|
connList: result.connList // Liste des files/applications
|
|
};
|
|
|
|
// Traitement des URLs des applications
|
|
processApplicationUrls(result.connList);
|
|
|
|
return { success: true, data: result };
|
|
}
|
|
|
|
return { success: false, message: 'Échec de connexion' };
|
|
} catch (error) {
|
|
console.error('Erreur login:', error);
|
|
return { success: false, message: error.message };
|
|
}
|
|
}
|
|
|
|
// Traitement des URLs avec placeholders
|
|
function processApplicationUrls(connList) {
|
|
return connList.map(conn => {
|
|
let url = conn.applicationName;
|
|
|
|
// Remplacement des placeholders
|
|
url = url.replace("#CA#", conn.accessCode);
|
|
url = url.replace("#MP#", conn.password);
|
|
|
|
// Gestion des cas spécifiques
|
|
if (url === "pro.mondocteur.fr") {
|
|
url = "https://pro.mondocteur.fr/backoffice.do";
|
|
}
|
|
if (url === "pro.doctolib.fr") {
|
|
url = "https://pro.doctolib.fr/signin";
|
|
}
|
|
|
|
return {
|
|
id: conn.code,
|
|
url: url,
|
|
credentials: {
|
|
accessCode: conn.accessCode,
|
|
password: conn.password
|
|
}
|
|
};
|
|
});
|
|
}
|
|
```
|
|
|
|
### 3. Gestion des événements IPBX en temps réel
|
|
|
|
```javascript
|
|
// Écouter les événements téléphoniques
|
|
function setupIpbxEventListener(connection, mainWindow) {
|
|
connection.on('IpbxEvent', (name, args) => {
|
|
if (!args || !global.AgentConnectionInfo) return;
|
|
|
|
const event = args[0];
|
|
console.log(`Événement IPBX reçu:
|
|
Code: ${event.eventCode}
|
|
Terminal: ${event.terminal}
|
|
File: ${event.queueName}
|
|
`);
|
|
|
|
handleIpbxEvent(event, mainWindow);
|
|
});
|
|
}
|
|
|
|
// Traitement des événements
|
|
function handleIpbxEvent(event, mainWindow) {
|
|
switch (event.eventCode) {
|
|
case 1: // Appel décroché
|
|
handleCallPickedUp(event, mainWindow);
|
|
break;
|
|
|
|
case 2: // Appel raccroché
|
|
handleCallHungUp(event, mainWindow);
|
|
break;
|
|
|
|
default:
|
|
console.log('Code événement non géré:', event.eventCode);
|
|
}
|
|
}
|
|
|
|
// Basculement automatique vers la bonne file
|
|
function handleCallPickedUp(event, mainWindow) {
|
|
// Identifier le centre correspondant à la file
|
|
const centre = identifyCentreByQueue(event.queueName);
|
|
|
|
if (centre) {
|
|
// Envoyer l'instruction de basculement à la fenêtre principale
|
|
mainWindow.webContents.send('switchToCenter', {
|
|
centreId: centre.id,
|
|
queueName: event.queueName,
|
|
terminal: event.terminal
|
|
});
|
|
|
|
// Logger l'événement
|
|
logCallEvent('CALL_PICKUP', event);
|
|
}
|
|
}
|
|
|
|
// Libération de la file après raccrochage
|
|
function handleCallHungUp(event, mainWindow) {
|
|
mainWindow.webContents.send('releaseCenter', {
|
|
queueName: event.queueName
|
|
});
|
|
|
|
// Logger l'événement
|
|
logCallEvent('CALL_HANGUP', event);
|
|
}
|
|
```
|
|
|
|
### 4. Récupération et sélection des terminaux téléphoniques
|
|
|
|
#### Processus complet de récupération des terminaux
|
|
|
|
La récupération des terminaux se fait en deux phases : récupération depuis le serveur et affichage dans l'interface de connexion.
|
|
|
|
##### Côté processus principal (main.js)
|
|
|
|
```javascript
|
|
// Handler IPC pour récupérer les terminaux
|
|
ipcMain.handle('get-terminal-list', async () => {
|
|
try {
|
|
const terminals = await signalRConnection.invoke(
|
|
'GetTerminalListByServiceProvider',
|
|
config.ServiceProvider // Ex: 'RDVPREM' depuis la config
|
|
);
|
|
|
|
console.log('Terminaux disponibles:', terminals);
|
|
// Retourne un tableau de numéros: ["3001", "3002", "3003", ...]
|
|
return terminals;
|
|
} catch (error) {
|
|
console.error('Erreur récupération terminaux:', error);
|
|
return [];
|
|
}
|
|
});
|
|
```
|
|
|
|
##### Côté renderer - Interface de sélection
|
|
|
|
```javascript
|
|
// Au chargement de la page de connexion
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
try {
|
|
// Afficher un loader
|
|
const terminalSelect = document.getElementById('terminal-select');
|
|
terminalSelect.innerHTML = '<option value="">Chargement des terminaux...</option>';
|
|
|
|
// Récupérer les terminaux disponibles depuis le serveur
|
|
const terminals = await ipcRenderer.invoke('get-terminal-list');
|
|
|
|
if (terminals && terminals.length > 0) {
|
|
populateTerminalSelect(terminals);
|
|
|
|
// Restaurer la dernière sélection si disponible
|
|
const lastTerminal = localStorage.getItem('last-terminal');
|
|
if (lastTerminal && terminals.includes(lastTerminal)) {
|
|
terminalSelect.value = lastTerminal;
|
|
}
|
|
} else {
|
|
showError('Aucun terminal téléphonique disponible');
|
|
terminalSelect.innerHTML = '<option value="">Aucun terminal disponible</option>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur chargement terminaux:', error);
|
|
showError('Impossible de récupérer la liste des terminaux');
|
|
}
|
|
});
|
|
|
|
// Remplir le select avec les terminaux
|
|
function populateTerminalSelect(terminals) {
|
|
const select = document.getElementById('terminal-select');
|
|
select.innerHTML = '<option value="">Sélectionner un terminal...</option>';
|
|
|
|
terminals.forEach(terminal => {
|
|
const option = document.createElement('option');
|
|
option.value = terminal;
|
|
option.textContent = `Poste ${terminal}`;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// Intégration dans le formulaire de connexion
|
|
async function handleLogin(e) {
|
|
e.preventDefault();
|
|
|
|
const terminal = document.getElementById('terminal-select').value;
|
|
|
|
// Validation : terminal obligatoire
|
|
if (!terminal) {
|
|
showError('Veuillez sélectionner un terminal téléphonique');
|
|
return;
|
|
}
|
|
|
|
const credentials = {
|
|
email: document.getElementById('email').value,
|
|
password: document.getElementById('password').value,
|
|
terminal: terminal
|
|
};
|
|
|
|
// Sauvegarder le terminal pour la prochaine connexion
|
|
localStorage.setItem('last-terminal', terminal);
|
|
|
|
// Connexion avec le terminal sélectionné
|
|
const result = await ipcRenderer.invoke('agent-login', credentials);
|
|
|
|
if (result.success) {
|
|
// Le terminal est maintenant associé à la session
|
|
// Il sera affiché dans le titre de la fenêtre
|
|
console.log(`Connecté sur le poste ${terminal}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
##### Interface HTML correspondante
|
|
|
|
```html
|
|
<!-- Formulaire de connexion avec sélection du terminal -->
|
|
<form id="loginForm">
|
|
<div class="form-group">
|
|
<label for="email">Email</label>
|
|
<input type="email" id="email" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Mot de passe</label>
|
|
<input type="password" id="password" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="terminal-select">Terminal téléphonique</label>
|
|
<select id="terminal-select" required>
|
|
<option value="">Chargement...</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button type="submit">Se connecter</button>
|
|
</form>
|
|
```
|
|
|
|
#### Points importants sur les terminaux
|
|
|
|
1. **ServiceProvider** : Le paramètre configuré dans `config.json` détermine quels terminaux sont disponibles pour l'organisation
|
|
|
|
2. **Filtrage serveur** : Le serveur filtre automatiquement les terminaux selon :
|
|
- Le fournisseur de service (ServiceProvider)
|
|
- Les droits de l'organisation
|
|
- La disponibilité des postes
|
|
- Les terminaux déjà utilisés par d'autres agents
|
|
|
|
3. **Validation obligatoire** : Un agent DOIT sélectionner un terminal pour se connecter - c'est ce qui permet le routage des appels
|
|
|
|
4. **Affichage contextuel** : Après connexion, le terminal est affiché dans le titre :
|
|
```javascript
|
|
mainWindow.setTitle(`SimpleConnect - Agent: ${user} (${firstName} ${lastName}) - Tel: ${terminal}`);
|
|
```
|
|
|
|
5. **Association terminal-agent** : Cette association permet au serveur SignalR de router les événements IPBX vers le bon agent
|
|
|
|
### 5. Déconnexion propre
|
|
|
|
```javascript
|
|
// Déconnexion agent
|
|
async function agentLogoff(connection, userId) {
|
|
try {
|
|
await connection.invoke('AgentLogoff', userId);
|
|
console.log('Agent déconnecté');
|
|
|
|
// Nettoyer les données locales
|
|
global.AgentConnectionInfo = null;
|
|
} catch (error) {
|
|
console.error('Erreur déconnexion:', error);
|
|
}
|
|
}
|
|
|
|
// Arrêt de la connexion SignalR
|
|
async function stopConnection(connection) {
|
|
if (connection.state === signalR.HubConnectionState.Connected) {
|
|
await connection.stop();
|
|
console.log('Connexion SignalR fermée');
|
|
}
|
|
}
|
|
```
|
|
|
|
## Intégration dans l'application Electron
|
|
|
|
### Dans le processus principal (main.js)
|
|
|
|
```javascript
|
|
const { app, BrowserWindow, ipcMain } = require('electron');
|
|
const signalR = require("@microsoft/signalr");
|
|
|
|
let signalRConnection = null;
|
|
let mainWindow = null;
|
|
|
|
app.on('ready', async () => {
|
|
// Créer la fenêtre principale
|
|
mainWindow = createMainWindow();
|
|
|
|
// Charger la configuration
|
|
const config = loadConfig();
|
|
|
|
// Initialiser SignalR
|
|
signalRConnection = createSignalRConnection(config.ServerIp);
|
|
|
|
// Configurer les listeners
|
|
setupIpbxEventListener(signalRConnection, mainWindow);
|
|
|
|
// Démarrer la connexion
|
|
const connected = await startConnection(signalRConnection);
|
|
|
|
if (connected) {
|
|
mainWindow.webContents.send('signalr-connected');
|
|
}
|
|
});
|
|
|
|
// Gestion des IPC depuis le renderer
|
|
ipcMain.handle('agent-login', async (event, credentials) => {
|
|
return await agentLogin(signalRConnection, credentials);
|
|
});
|
|
|
|
ipcMain.handle('get-terminals', async (event) => {
|
|
return await getTerminalList(signalRConnection, config.ServiceProvider);
|
|
});
|
|
|
|
// Nettoyage à la fermeture
|
|
app.on('before-quit', async () => {
|
|
if (global.AgentConnectionInfo) {
|
|
await agentLogoff(signalRConnection, global.AgentConnectionInfo.accessCode);
|
|
}
|
|
await stopConnection(signalRConnection);
|
|
});
|
|
```
|
|
|
|
### Dans le processus renderer (renderer.js)
|
|
|
|
```javascript
|
|
const { ipcRenderer } = require('electron');
|
|
|
|
// Écouter la connexion SignalR
|
|
ipcRenderer.on('signalr-connected', () => {
|
|
console.log('SignalR connecté');
|
|
updateConnectionStatus('connected');
|
|
});
|
|
|
|
// Écouter les changements de file
|
|
ipcRenderer.on('switchToCenter', (event, data) => {
|
|
// Basculer automatiquement vers le bon centre
|
|
selectCenter(data.centreId);
|
|
|
|
// Afficher une notification
|
|
showNotification(`Appel entrant sur ${data.queueName}`);
|
|
|
|
// Mettre à jour l'interface
|
|
updateCallStatus('in-call', data);
|
|
});
|
|
|
|
// Libération de file
|
|
ipcRenderer.on('releaseCenter', (event, data) => {
|
|
updateCallStatus('available');
|
|
|
|
// Optionnel : revenir à l'écran d'accueil
|
|
showHomeScreen();
|
|
});
|
|
```
|
|
|
|
## Flux de données complet
|
|
|
|
### Séquence d'un appel type
|
|
|
|
1. **Appel entrant** → Système IPBX
|
|
2. **Événement généré** → Serveur SignalR
|
|
3. **IpbxEvent transmis** → Client Electron (eventCode: 1)
|
|
4. **Identification de la file** → Mapping file/centre
|
|
5. **Basculement automatique** → Webview du centre concerné
|
|
6. **Agent traite l'appel** → Prise de RDV
|
|
7. **Fin d'appel** → Système IPBX
|
|
8. **IpbxEvent transmis** → Client Electron (eventCode: 2)
|
|
9. **Libération de la file** → Retour état disponible
|
|
|
|
## Monitoring et logs
|
|
|
|
```javascript
|
|
// Structure de log pour dashboard
|
|
function createDashboardLog(agentInfo, terminals, connection) {
|
|
return {
|
|
"PrestaConnect": {
|
|
"Ouverture": {
|
|
"Ouvert": "Oui",
|
|
"Date": new Date().toISOString(),
|
|
"IP_Client": getLocalIP(),
|
|
"IP_Serveur": config.ServerIp,
|
|
"Liste_Telephones": terminals
|
|
},
|
|
"Connexion": {
|
|
"Connecte": agentInfo ? "Oui" : "Non",
|
|
"Agent": agentInfo?.accessCode,
|
|
"Telephone": agentInfo?.terminal,
|
|
"Nom_Agent": `${agentInfo?.firstName} ${agentInfo?.lastName}`,
|
|
"Nombre_Files": agentInfo?.connList.length,
|
|
"Files": agentInfo?.connList
|
|
},
|
|
"SignalR": {
|
|
"État": connection.state,
|
|
"Reconnexions": connection.reconnectAttempts || 0
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Écriture du log
|
|
function writeDashboardLog(logData) {
|
|
const fs = require('fs');
|
|
fs.writeFileSync(
|
|
'./log/log-Dashboard.json',
|
|
JSON.stringify(logData, null, 2)
|
|
);
|
|
}
|
|
```
|
|
|
|
## Points d'attention pour l'implémentation
|
|
|
|
### 1. Gestion de la reconnexion
|
|
- Implémenter une stratégie de reconnexion automatique
|
|
- Gérer les états de connexion dans l'interface
|
|
- Sauvegarder l'état local pour restauration après reconnexion
|
|
|
|
### 2. Sécurité
|
|
- Ne jamais exposer les credentials en clair dans les logs
|
|
- Utiliser HTTPS pour la connexion SignalR en production
|
|
- Implémenter un timeout de session
|
|
|
|
### 3. Performance
|
|
- Limiter le nombre de listeners SignalR actifs
|
|
- Nettoyer les listeners lors des changements de contexte
|
|
- Implémenter un système de cache pour les données statiques
|
|
|
|
### 4. Gestion d'erreurs
|
|
- Capturer toutes les erreurs de connexion
|
|
- Afficher des messages utilisateur clairs
|
|
- Logger tous les événements pour debug
|
|
|
|
## Tests recommandés
|
|
|
|
1. **Test de connexion/déconnexion**
|
|
2. **Test de basculement automatique entre files**
|
|
3. **Test de perte de connexion réseau**
|
|
4. **Test de charge avec appels multiples**
|
|
5. **Test de synchronisation multi-agents**
|
|
|
|
## Prochaines étapes
|
|
|
|
1. Adapter le code SignalR au contexte de notre application
|
|
2. Configurer le serveur SignalR backend
|
|
3. Mapper les centres médicaux avec les files téléphoniques
|
|
4. Implémenter l'interface de monitoring temps réel
|
|
5. Tester l'intégration complète avec le système IPBX |