const { app, BrowserWindow, ipcMain, session } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const signalR = require('@microsoft/signalr'); const ConnectionManager = require('./connection-manager'); 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 // Configuration du système de logs SignalR const SIGNALR_LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng'); const SIGNALR_LOG_FILE = path.join(SIGNALR_LOG_DIR, 'signalr.log'); // Créer le répertoire de logs s'il n'existe pas function ensureLogDirectory() { if (!fs.existsSync(SIGNALR_LOG_DIR)) { fs.mkdirSync(SIGNALR_LOG_DIR, { recursive: true }); console.log(`📁 Répertoire de logs créé: ${SIGNALR_LOG_DIR}`); } } // Fonction pour écrire dans le fichier de log SignalR function logToSignalRFile(message, data = null) { ensureLogDirectory(); const timestamp = new Date().toISOString(); let logEntry = `[${timestamp}] ${message}`; if (data) { logEntry += '\n' + JSON.stringify(data, null, 2); } logEntry += '\n' + '─'.repeat(80) + '\n'; // Ajouter au fichier (append) fs.appendFileSync(SIGNALR_LOG_FILE, logEntry, 'utf8'); } // Logger dans la console ET dans le fichier function logSignalR(message, data = null) { console.log(message, data || ''); logToSignalRFile(message, data); } // 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', autoHideMenuBar: true // Cache la barre de menu par défaut }); // Supprimer complètement le menu (pour Linux/Windows) mainWindow.setMenuBarVisibility(false); // 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/WEBSOCKET === function initializeSignalR() { if (!config.signalR || !config.signalR.enabled) { console.log('SignalR/WebSocket désactivé dans la configuration'); signalRStatus = 'disabled'; sendSignalRStatus(); return; } try { // Utiliser le ConnectionManager avec fallback automatique SignalR → WebSocket const connectionManager = new ConnectionManager(config.ServerIp || config.signalR.serverUrl.replace('http://', '').replace('/signalR', '')); // La connexion sera établie plus tard avec fallback automatique signalRConnection = connectionManager; // Les handlers d'état seront configurés après la connexion // Démarrer la connexion (les handlers seront configurés après) startSignalRConnection(); } catch (error) { console.error('Erreur initialisation SignalR:', error); signalRStatus = 'error'; sendSignalRStatus(); } } function setupSignalRHandlers() { // Configuration des handlers après la connexion const connection = signalRConnection.getConnection(); if (!connection) { console.error('Pas de connexion active pour configurer les handlers'); return; } // === LOGGER UNIVERSEL POUR TOUS LES MESSAGES SIGNALR === // Intercepter TOUS les messages reçus du serveur pour découvrir les événements disponibles // Initialiser le fichier de log avec une session logSignalR('════════════════════════════════════════════════════════════'); logSignalR('🚀 NOUVELLE SESSION SIGNALR DÉMARRÉE'); logSignalR(`Application: SimpleConnect v${app.getVersion()}`); logSignalR(`Serveur SignalR: ${config.signalR.serverUrl}`); logSignalR(`Service Provider: ${config.signalR.serviceProvider}`); logSignalR('════════════════════════════════════════════════════════════'); // Liste des événements connus pour les logger différemment const knownEvents = ['IpbxEvent']; // Créer un proxy pour intercepter tous les appels .on() const originalOn = signalRConnection.on.bind(signalRConnection); // Logger tous les événements possibles en essayant d'écouter les plus communs const possibleEvents = [ 'IpbxEvent', // Événements téléphoniques (confirmé) 'AgentStatusChanged', // Changement de statut agent 'QueueUpdate', // Mise à jour des files 'CallReceived', // Appel entrant 'CallEnded', // Fin d'appel 'MessageReceived', // Messages 'Notification', // Notifications générales 'StatusUpdate', // Mises à jour de statut 'SystemMessage', // Messages système 'BroadcastMessage', // Messages broadcast 'AgentUpdate', // Mises à jour agent 'QueueStatistics', // Statistiques de file 'PresenceUpdate' // Mise à jour de présence ]; // Écouter tous les événements possibles et logger ce qu'on reçoit possibleEvents.forEach(eventName => { connection.on(eventName, (...args) => { // Logger dans la console avec formatage console.log('═══════════════════════════════════════════════════════════'); console.log(`📨 MESSAGE SIGNALR REÇU: ${eventName}`); console.log('Timestamp:', new Date().toISOString()); console.log('Nombre d\'arguments:', args.length); // Logger chaque argument en détail args.forEach((arg, index) => { console.log(`Argument ${index}:`, JSON.stringify(arg, null, 2)); }); console.log('═══════════════════════════════════════════════════════════'); // Logger dans le fichier avec structure const logData = { event: eventName, timestamp: new Date().toISOString(), argumentCount: args.length, arguments: args.map((arg, index) => ({ index: index, type: typeof arg, value: arg })), agent: currentAgent ? { id: currentAgent.id, name: currentAgent.name, terminal: currentTerminal } : null }; logSignalR(`📨 MESSAGE SIGNALR REÇU: ${eventName}`, logData); // Si c'est IpbxEvent, traiter comme avant if (eventName === 'IpbxEvent') { handleIpbxEventOriginal(args); } }); }); // Fonction originale pour traiter les IpbxEvent function handleIpbxEventOriginal(args) { if (!args || !agentConnectionInfo) return; const [name, eventArgs] = args; const event = eventArgs?.[0] || args[0]; console.log('🔍 Traitement IpbxEvent:', { eventCode: event.eventCode, terminal: event.terminal, queueName: event.queueName, fullEvent: event // Logger l'objet complet pour voir tous les champs }); // 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 0: console.log('📞 Code 0: Appel entrant/sonnerie (non implémenté)'); break; case 1: // Appel décroché console.log('✅ Code 1: Appel décroché - Traitement...'); handleCallPickedUp(event); break; case 2: // Appel raccroché console.log('📴 Code 2: Appel raccroché - Traitement...'); handleCallHungUp(event); break; case 3: console.log('⏸️ Code 3: Mise en attente (non implémenté)'); break; case 4: console.log('↔️ Code 4: Transfert d\'appel (non implémenté)'); break; case 5: console.log('👥 Code 5: Conférence (non implémenté)'); break; default: console.log('❓ Code événement inconnu:', event.eventCode); console.log('Données complètes:', JSON.stringify(event, null, 2)); } } // Logger aussi les méthodes qu'on peut invoquer console.log('📋 MÉTHODES SIGNALR DISPONIBLES POUR INVOCATION:'); console.log('- AgentLogin(email, password, terminal)'); console.log('- AgentLogoff(accessCode)'); console.log('- GetTerminalListByServiceProvider(serviceProvider)'); console.log('ℹ️ D\'autres méthodes peuvent être disponibles sur le serveur'); } // 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(); logSignalR('🔌 Tentative de connexion au serveur...', { serverUrl: config.ServerIp || config.signalR.serverUrl, status: 'connecting' }); // Le ConnectionManager gère le fallback automatiquement const connection = await signalRConnection.connect(); // Déterminer quel type de connexion a réussi const connectionInfo = signalRConnection.getConnectionInfo(); console.log(`Connexion établie via ${connectionInfo.type}`); logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, { connectionType: connectionInfo.type, isWebSocketFallback: connectionInfo.isWebSocketFallback, status: 'connected', serverUrl: connectionInfo.serverUrl }); // Maintenant configurer les handlers sur la connexion active setupSignalRHandlers(); signalRStatus = 'connected'; sendSignalRStatus(); } catch (error) { console.error('Erreur connexion SignalR:', error); logSignalR('❌ Erreur de connexion SignalR', { error: error.message, stack: error.stack, serverUrl: config.signalR.serverUrl }); 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(() => { // Supprimer le menu de l'application complètement (toutes plateformes) const { Menu } = require('electron'); Menu.setApplicationMenu(null); // 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); logSignalR('📡 Invocation SignalR: GetTerminalListByServiceProvider', { method: 'GetTerminalListByServiceProvider', serviceProvider: config.signalR.serviceProvider }); const terminals = await signalRConnection.invoke( 'GetTerminalListByServiceProvider', config.signalR.serviceProvider ); console.log('Terminaux disponibles:', terminals); logSignalR('📞 Terminaux récupérés', { count: terminals.length, terminals: 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); logSignalR('🔐 Tentative de connexion agent', { email: credentials.email, terminal: credentials.terminal, forceDisconnect: credentials.forceDisconnect || false }); // Si déconnexion forcée demandée, déconnecter d'abord la session précédente if (credentials.forceDisconnect) { console.log('Déconnexion forcée demandée pour:', credentials.email); try { // Tenter la déconnexion avec le code d'accès await signalRConnection.invoke('AgentLogoff', credentials.email); console.log('Session précédente déconnectée avec succès'); logSignalR('🔓 Session précédente déconnectée (forceDisconnect)', { email: credentials.email }); } catch (logoffError) { console.warn('Erreur lors de la déconnexion forcée (session peut-être déjà fermée):', logoffError.message); logSignalR('⚠️ Erreur déconnexion forcée', { email: credentials.email, error: logoffError.message }); // Continuer même si la déconnexion échoue - la session est peut-être déjà fermée } } // Appel SignalR pour l'authentification logSignalR('📡 Invocation SignalR: AgentLogin', { method: 'AgentLogin', email: credentials.email, terminal: credentials.terminal }); const result = await signalRConnection.invoke('AgentLogin', credentials.email, credentials.password, credentials.terminal ); if (result) { console.log('Connexion réussie:', result); logSignalR('✅ Connexion agent réussie', { accessCode: result.accessCode, firstName: result.firstName, lastName: result.lastName, terminal: credentials.terminal, connListCount: result.connList ? result.connList.length : 0, connList: result.connList }); // 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 logSignalR('📡 Invocation SignalR: AgentLogoff', { method: 'AgentLogoff', accessCode: currentAgent.accessCode }); await signalRConnection.invoke('AgentLogoff', currentAgent.accessCode); console.log('Agent déconnecté du serveur SignalR'); logSignalR('👋 Agent déconnecté avec succès', { accessCode: currentAgent.accessCode, name: currentAgent.name }); } 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 }; }); // Handler pour quitter l'application proprement ipcMain.handle('quit-app', async () => { // Fermer la connexion SignalR si elle existe if (signalRConnection) { try { await signalRConnection.stop(); console.log('Connexion SignalR fermée'); } catch (error) { console.error('Erreur lors de la fermeture de SignalR:', error); } } // Quitter l'application app.quit(); }); // 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 (un seul fichier par agent) ipcMain.handle('save-notes', (event, noteData) => { const notesDir = path.join(__dirname, 'notes'); if (!fs.existsSync(notesDir)) { fs.mkdirSync(notesDir); } // Un seul fichier par agent, mis à jour à chaque sauvegarde const fileName = `notes_${currentAgent.id}.json`; const filePath = path.join(notesDir, fileName); // Lire l'historique existant si le fichier existe let notesData = { agent: currentAgent.id, agentName: currentAgent.name, currentNote: noteData.content, lastModified: new Date().toISOString(), centre: noteData.centre, history: [] }; // Si le fichier existe, préserver l'historique if (fs.existsSync(filePath)) { try { const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8')); // Ajouter l'ancienne note à l'historique si elle a changé if (existingData.currentNote && existingData.currentNote !== noteData.content) { notesData.history = existingData.history || []; notesData.history.unshift({ content: existingData.currentNote, date: existingData.lastModified, centre: existingData.centre }); // Limiter l'historique à 50 entrées notesData.history = notesData.history.slice(0, 50); } } catch (error) { console.error('Erreur lecture notes existantes:', error); } } fs.writeFileSync(filePath, JSON.stringify(notesData, null, 2)); return { success: true, file: fileName }; }); // Récupérer les notes de l'agent ipcMain.handle('get-notes', () => { if (!currentAgent) return null; const notesDir = path.join(__dirname, 'notes'); const fileName = `notes_${currentAgent.id}.json`; const filePath = path.join(notesDir, fileName); if (fs.existsSync(filePath)) { try { const data = fs.readFileSync(filePath, 'utf8'); return JSON.parse(data); } catch (error) { console.error('Erreur lecture notes:', error); return null; } } return null; }); // 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'; });