const { app, BrowserWindow, ipcMain, session } = require('electron'); const path = require('path'); const fs = require('fs'); const signalR = require('@microsoft/signalr'); let mainWindow; let config; let currentAgent = null; let currentTerminal = null; // Terminal téléphonique de l'agent connecté let signalRConnection = null; let signalRStatus = 'disconnected'; // disconnected, connecting, connected, error let agentConnectionInfo = null; // Informations complètes retournées par SignalR // Charger la configuration function loadConfig() { const configPath = path.join(__dirname, 'config.json'); const configData = fs.readFileSync(configPath, 'utf8'); config = JSON.parse(configData); } // Créer la fenêtre principale function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, webPreferences: { nodeIntegration: true, contextIsolation: false, webviewTag: true, webSecurity: false }, icon: path.join(__dirname, 'icon.png'), title: 'SimpleConnect - Gestion Centralisée des Plannings' }); // Charger l'interface HTML mainWindow.loadFile('index.html'); // Ouvrir les DevTools uniquement en mode développement if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools(); } // Gérer la fermeture de la fenêtre mainWindow.on('closed', () => { mainWindow = null; }); } // === GESTION SIGNALR === function initializeSignalR() { if (!config.signalR || !config.signalR.enabled) { console.log('SignalR désactivé dans la configuration'); signalRStatus = 'disabled'; sendSignalRStatus(); return; } try { // Créer la connexion SignalR signalRConnection = new signalR.HubConnectionBuilder() .withUrl(config.signalR.serverUrl) .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) .configureLogging(signalR.LogLevel.Information) .build(); // Gérer les changements d'état signalRConnection.onreconnecting(() => { console.log('SignalR: Reconnexion en cours...'); signalRStatus = 'connecting'; sendSignalRStatus(); }); signalRConnection.onreconnected(() => { console.log('SignalR: Reconnecté'); signalRStatus = 'connected'; sendSignalRStatus(); }); signalRConnection.onclose(() => { console.log('SignalR: Connexion fermée'); signalRStatus = 'disconnected'; sendSignalRStatus(); }); // Configurer les méthodes SignalR setupSignalRMethods(); // Démarrer la connexion startSignalRConnection(); } catch (error) { console.error('Erreur initialisation SignalR:', error); signalRStatus = 'error'; sendSignalRStatus(); } } function setupSignalRMethods() { // Écouter les événements IPBX signalRConnection.on('IpbxEvent', (name, args) => { if (!args || !agentConnectionInfo) return; const event = args[0]; console.log('Événement IPBX reçu:', { eventCode: event.eventCode, terminal: event.terminal, queueName: event.queueName }); // Vérifier que l'événement est pour notre terminal if (event.terminal !== currentTerminal) { console.log('Événement ignoré - Terminal différent:', event.terminal, '!==', currentTerminal); return; } // Gérer les différents types d'événements switch(event.eventCode) { case 1: // Appel décroché handleCallPickedUp(event); break; case 2: // Appel raccroché handleCallHungUp(event); break; default: console.log('Code événement non géré:', event.eventCode); } }); } // Gérer un appel entrant function handleCallPickedUp(event) { if (!mainWindow || !agentConnectionInfo) return; // Identifier le centre correspondant à la file const centres = processApplicationUrls(agentConnectionInfo.connList); const centre = centres.find(c => c.queueName === event.queueName); if (centre) { console.log('Basculement vers le centre:', centre.nom); // Envoyer l'instruction de basculement à la fenêtre mainWindow.webContents.send('switch-to-center', { centreId: centre.id, centreName: centre.nom, queueName: event.queueName, terminal: event.terminal, eventType: 'call_pickup' }); } else { console.warn('Aucun centre trouvé pour la file:', event.queueName); } } // Gérer la fin d'un appel function handleCallHungUp(event) { if (!mainWindow) return; console.log('Fin d\'appel sur la file:', event.queueName); // Envoyer l'instruction de libération à la fenêtre mainWindow.webContents.send('release-center', { queueName: event.queueName, terminal: event.terminal, eventType: 'call_hangup' }); } async function startSignalRConnection() { try { signalRStatus = 'connecting'; sendSignalRStatus(); await signalRConnection.start(); console.log('SignalR: Connexion établie'); signalRStatus = 'connected'; sendSignalRStatus(); } catch (error) { console.error('Erreur connexion SignalR:', error); signalRStatus = 'error'; sendSignalRStatus(); // Réessayer dans 5 secondes setTimeout(() => { if (signalRConnection && signalRStatus !== 'connected') { startSignalRConnection(); } }, 5000); } } function sendSignalRStatus() { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('signalr-status', signalRStatus); } } // Initialisation de l'application app.whenReady().then(() => { // Configuration de la session pour éviter les problèmes CORS session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { details.requestHeaders['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; callback({ requestHeaders: details.requestHeaders }); }); session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': ['default-src * \'unsafe-inline\' \'unsafe-eval\' data: blob:;'] } }); }); loadConfig(); createWindow(); // Initialiser SignalR après le chargement de la config initializeSignalR(); }); // Quitter quand toutes les fenêtres sont fermées app.on('window-all-closed', async () => { // Déconnexion propre avant de quitter if (currentAgent && signalRConnection && signalRStatus === 'connected') { try { await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode); console.log('Déconnexion agent avant fermeture'); } catch (error) { console.error('Erreur déconnexion:', error); } } // Arrêter SignalR if (signalRConnection) { await signalRConnection.stop(); } if (process.platform !== 'darwin') { app.quit(); } }); // Réactiver l'app sur macOS app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); // === IPC HANDLERS === // Obtenir la configuration ipcMain.handle('get-config', () => { return config; }); // Obtenir le statut SignalR ipcMain.handle('get-signalr-status', () => { return signalRStatus; }); // Récupérer la liste des terminaux téléphoniques ipcMain.handle('get-terminal-list', async () => { // Mode simulation si SignalR non connecté if (!signalRConnection || signalRStatus !== 'connected') { console.log('SignalR non connecté, utilisation des terminaux de simulation'); return config.signalR.terminalsSimulation || ['3001', '3002', '3003']; } try { console.log('Récupération des terminaux pour:', config.signalR.serviceProvider); const terminals = await signalRConnection.invoke( 'GetTerminalListByServiceProvider', config.signalR.serviceProvider ); console.log('Terminaux disponibles:', terminals); return terminals || []; } catch (error) { console.error('Erreur récupération terminaux:', error); // Retourner les terminaux de simulation en cas d'erreur return config.signalR.terminalsSimulation || ['3001', '3002', '3003']; } }); // Connexion agent via SignalR ipcMain.handle('login-agent', async (event, credentials) => { // Vérifier que SignalR est connecté if (!signalRConnection || signalRStatus !== 'connected') { return { success: false, message: 'Connexion au serveur SignalR non établie. Veuillez réessayer.' }; } try { console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal); // Appel SignalR pour l'authentification const result = await signalRConnection.invoke('AgentLogin', credentials.email, credentials.password, credentials.terminal ); if (result) { console.log('Connexion réussie:', result); // Stocker les informations de connexion agentConnectionInfo = result; currentTerminal = credentials.terminal; // Créer l'objet agent pour compatibilité currentAgent = { id: result.accessCode, accessCode: result.accessCode, name: `${result.firstName} ${result.lastName}`, email: credentials.email, firstName: result.firstName, lastName: result.lastName, terminal: credentials.terminal }; // Traiter les URLs des applications et créer les centres const centres = processApplicationUrls(result.connList); // Mettre à jour le titre de la fenêtre if (mainWindow) { mainWindow.setTitle( `SimpleConnect - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}` ); } return { success: true, agent: currentAgent, centres: centres }; } return { success: false, message: 'Échec de l\'authentification' }; } catch (error) { console.error('Erreur lors de la connexion agent:', error); return { success: false, message: error.message || 'Erreur de connexion au serveur' }; } }); // Traiter les URLs des applications avec les placeholders function processApplicationUrls(connList) { if (!connList || connList.length === 0) return []; return connList.map((conn, index) => { let url = conn.applicationName; // Remplacer les placeholders if (url.includes('#CA#')) { url = url.replace('#CA#', conn.accessCode || ''); } if (url.includes('#MP#')) { url = url.replace('#MP#', conn.password || ''); } // Gérer les cas spécifiques des plateformes connues if (url === 'pro.mondocteur.fr' || url.includes('mondocteur.fr')) { if (!url.startsWith('http')) { url = 'https://pro.mondocteur.fr/backoffice.do'; } } else if (url === 'pro.doctolib.fr' || url.includes('doctolib.fr')) { if (!url.startsWith('http')) { url = 'https://pro.doctolib.fr/signin'; } } else if (!url.startsWith('http')) { // Ajouter https:// par défaut si pas de protocole url = `https://${url}`; } // Créer l'objet centre compatible avec l'interface return { id: conn.code || `centre${index + 1}`, nom: conn.queueName || conn.code || `Centre ${index + 1}`, url: url, couleur: getColorForIndex(index), credentials: { username: conn.accessCode, password: conn.password }, queueName: conn.queueName // Garder pour le mapping avec les événements IPBX }; }); } // Fonction helper pour attribuer des couleurs aux centres function getColorForIndex(index) { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77']; return colors[index % colors.length]; } // Déconnexion agent via SignalR ipcMain.handle('logout', async () => { if (currentAgent && signalRConnection && signalRStatus === 'connected') { try { // Appeler SignalR pour la déconnexion await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode); console.log('Agent déconnecté du serveur SignalR'); } catch (error) { console.error('Erreur lors de la déconnexion SignalR:', error); } } // Réinitialiser les variables locales currentAgent = null; currentTerminal = null; agentConnectionInfo = null; // Réinitialiser le titre de la fenêtre if (mainWindow) { mainWindow.setTitle('SimpleConnect - Gestion Centralisée des Plannings'); } return { success: true }; }); // Obtenir l'agent actuel ipcMain.handle('get-current-agent', () => { if (!currentAgent || !agentConnectionInfo) return null; // Retourner les centres traités depuis SignalR const centres = processApplicationUrls(agentConnectionInfo.connList); return { agent: currentAgent, centres: centres, terminal: currentTerminal }; }); // Simuler un appel entrant ipcMain.handle('simulate-call', (event, callData) => { // Envoyer l'événement d'appel entrant à la fenêtre mainWindow.webContents.send('incoming-call', callData); return { success: true }; }); // Obtenir les données pour simuler des appels ipcMain.handle('get-simulated-calls', () => { return config.cti.appelSimules.map(appel => { const centre = config.centres.find(c => c.id === appel.centreId); return { ...appel, centreNom: centre ? centre.nom : 'Centre inconnu' }; }); }); // Sauvegarder les notes de l'agent ipcMain.handle('save-notes', (event, noteData) => { const notesDir = path.join(__dirname, 'notes'); if (!fs.existsSync(notesDir)) { fs.mkdirSync(notesDir); } const fileName = `notes_${currentAgent.id}_${Date.now()}.json`; const filePath = path.join(notesDir, fileName); fs.writeFileSync(filePath, JSON.stringify({ agent: currentAgent.id, timestamp: new Date().toISOString(), ...noteData }, null, 2)); return { success: true, file: fileName }; }); // Obtenir l'historique des appels ipcMain.handle('get-call-history', () => { const historyFile = path.join(__dirname, 'call_history.json'); if (fs.existsSync(historyFile)) { const data = fs.readFileSync(historyFile, 'utf8'); return JSON.parse(data); } return []; }); // Sauvegarder un appel dans l'historique ipcMain.handle('save-call-history', (event, callData) => { const historyFile = path.join(__dirname, 'call_history.json'); let history = []; if (fs.existsSync(historyFile)) { const data = fs.readFileSync(historyFile, 'utf8'); history = JSON.parse(data); } history.unshift({ ...callData, agentId: currentAgent?.id, timestamp: new Date().toISOString() }); // Garder seulement les 100 derniers appels history = history.slice(0, 100); fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); return { success: true }; }); // Vérifier si on est en mode développement ipcMain.handle('is-development', () => { return process.env.NODE_ENV === 'development'; });