Files
SimpleClient-releases/docs/INTEGRATION_SIGNALR.md
2025-09-04 11:43:50 -04:00

12 KiB

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

{
  "ServerIp": "10.90.20.201:8002",
  "ServiceProvider": "RDVPREM"
}

Installation des dépendances

npm install @microsoft/signalr

Implémentation du client SignalR

1. Connexion au Hub

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

// 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

// É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 de la liste des terminaux

// Obtenir les terminaux disponibles
async function getTerminalList(connection, serviceProvider) {
    try {
        const terminals = await connection.invoke(
            'GetTerminalListByServiceProvider',
            serviceProvider
        );
        
        console.log('Terminaux disponibles:', terminals);
        return terminals;
    } catch (error) {
        console.error('Erreur récupération terminaux:', error);
        return [];
    }
}

5. Déconnexion propre

// 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)

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)

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

// 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