const { app, BrowserWindow, ipcMain, session, net } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const SocketIOAdapter = require('./socketio-adapter'); let mainWindow; let config; let currentAgent = null; let currentTerminal = null; let adapter = null; let serverStatus = 'disconnected'; // disconnected, connecting, connected, error let agentConnectionInfo = null; let healthCheckInterval = null; // Configuration du systeme de logs const LOG_DIR = path.join(os.homedir(), '.simpleconnect-ng'); const LOG_FILE = path.join(LOG_DIR, 'socketio.log'); function ensureLogDirectory() { if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR, { recursive: true }); } } function logToFile(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'; fs.appendFileSync(LOG_FILE, logEntry, 'utf8'); } function log(message, data = null) { console.log(message, data || ''); logToFile(message, data); } // Charger la configuration function loadConfig() { const configPath = path.join(__dirname, 'config.json'); const configData = fs.readFileSync(configPath, 'utf8'); config = JSON.parse(configData); } // Creer la fenetre 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 v${app.getVersion()}`, autoHideMenuBar: true }); mainWindow.setMenuBarVisibility(false); mainWindow.loadFile('index.html'); mainWindow.webContents.on('did-finish-load', () => { mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); }); if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools(); } mainWindow.on('closed', () => { mainWindow = null; }); } // === GESTION SOCKET.IO === function initializeSocketIO() { if (!config.socketio || !config.socketio.serverUrl) { console.log('Socket.IO non configure'); serverStatus = 'disabled'; sendServerStatus(); return; } adapter = new SocketIOAdapter(config.socketio.serverUrl); // Demarrer le health check polling startHealthCheck(); } // Health check periodique via GET /health function startHealthCheck() { const checkHealth = () => { const serverUrl = config.socketio.serverUrl; const request = net.request(`${serverUrl}/health`); request.on('response', (response) => { if (response.statusCode === 200) { if (serverStatus !== 'connected') { // Ne passer en 'connected' que si on n'a pas d'agent connecte // (sinon le status est deja 'connected' via le login) if (!currentAgent) { serverStatus = 'connected'; sendServerStatus(); } } } else { serverStatus = 'error'; sendServerStatus(); } }); request.on('error', () => { serverStatus = 'error'; sendServerStatus(); }); request.end(); }; // Check immediat puis toutes les 10s checkHealth(); healthCheckInterval = setInterval(checkHealth, 10000); } function sendServerStatus() { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('server-status', serverStatus); } } // Gerer un appel entrant function handleCallPickedUp(event) { if (!mainWindow || !agentConnectionInfo) return; const centres = processApplicationUrls(agentConnectionInfo.connList); const centre = centres.find(c => c.queueName === event.queueName); if (centre) { console.log('Basculement vers le centre:', centre.nom); 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 trouve pour la file:', event.queueName); } } // Gerer la fin d'un appel function handleCallHungUp(event) { if (!mainWindow) return; console.log('Fin d\'appel sur la file:', event.queueName); mainWindow.webContents.send('release-center', { queueName: event.queueName, terminal: event.terminal, eventType: 'call_hangup' }); } // Configurer les handlers d'evenements Socket.IO apres connexion function setupEventHandlers() { if (!adapter) return; log('Session Socket.IO demarree', { serverUrl: config.socketio.serverUrl, serviceProvider: config.socketio.serviceProvider }); // Ecouter les evenements d'appels IPBX adapter.on('ipbx_event', (data) => { log('ipbx_event recu', data); if (!agentConnectionInfo) return; // Verifier que l'evenement est pour notre terminal if (data.terminal !== currentTerminal) { console.log('Evenement ignore - terminal different:', data.terminal, '!==', currentTerminal); return; } switch (data.eventCode) { case 1: handleCallPickedUp(data); break; case 2: handleCallHungUp(data); break; default: log('Code evenement non gere:', data); } }); } // Initialisation de l'application app.whenReady().then(() => { const { Menu } = require('electron'); Menu.setApplicationMenu(null); 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(); initializeSocketIO(); }); // Quitter quand toutes les fenetres sont fermees app.on('window-all-closed', async () => { if (healthCheckInterval) { clearInterval(healthCheckInterval); } // Deconnexion propre avant de quitter if (currentAgent && adapter) { try { await adapter.logoff(); console.log('Agent deconnecte avant fermeture'); } catch (error) { console.error('Erreur deconnexion:', error); } } if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); // === IPC HANDLERS === ipcMain.handle('get-config', () => { return config; }); ipcMain.handle('get-app-version', () => { return app.getVersion(); }); // Renomme : get-signalr-status -> get-server-status ipcMain.handle('get-server-status', () => { return serverStatus; }); // Recuperer la liste des terminaux via REST ipcMain.handle('get-terminal-list', async () => { if (!config.socketio || !config.socketio.serverUrl) { return []; } try { const provider = config.socketio.serviceProvider; const url = `${config.socketio.serverUrl}/terminals?provider=${encodeURIComponent(provider)}`; console.log('Recuperation des terminaux:', url); return new Promise((resolve, reject) => { const request = net.request(url); let body = ''; request.on('response', (response) => { response.on('data', (chunk) => { body += chunk.toString(); }); response.on('end', () => { if (response.statusCode === 200) { try { const terminals = JSON.parse(body); log('Terminaux recuperes', { count: terminals.length, terminals }); resolve(Array.isArray(terminals) ? terminals : []); } catch (e) { console.error('Erreur parsing terminaux:', e); resolve([]); } } else { console.error('Erreur terminaux HTTP', response.statusCode); resolve([]); } }); }); request.on('error', (error) => { console.error('Erreur recuperation terminaux:', error); resolve([]); }); request.end(); }); } catch (error) { console.error('Erreur recuperation terminaux:', error); return []; } }); // Connexion agent via Socket.IO ipcMain.handle('login-agent', async (event, credentials) => { if (!adapter) { return { success: false, message: 'Socket.IO non initialise. Veuillez reessayer.' }; } try { console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal); log('Tentative de connexion agent', { email: credentials.email, terminal: credentials.terminal, forceDisconnect: credentials.forceDisconnect || false }); // Deconnecter l'adapter precedent s'il y en a un if (adapter.state === 'connected') { adapter.disconnect(); } // Recreer l'adapter pour une connexion fraiche adapter = new SocketIOAdapter(config.socketio.serverUrl); // Configurer les handlers d'evenements AVANT connect setupEventHandlers(); // Connexion avec auth au handshake const result = await adapter.connect( credentials.email, credentials.password, credentials.terminal ); if (result) { console.log('Connexion reussie:', result); log('Connexion agent reussie', { accessCode: result.accessCode, firstName: result.firstName, lastName: result.lastName, terminal: credentials.terminal, connListCount: result.connList ? result.connList.length : 0, connList: result.connList }); agentConnectionInfo = result; currentTerminal = credentials.terminal; currentAgent = { id: result.accessCode, accessCode: result.accessCode, name: `${result.firstName} ${result.lastName}`, email: credentials.email, firstName: result.firstName, lastName: result.lastName, terminal: credentials.terminal }; const centres = processApplicationUrls(result.connList); if (mainWindow) { mainWindow.setTitle( `SimpleConnect v${app.getVersion()} - Agent: ${currentAgent.accessCode} (${result.firstName} ${result.lastName}) - Tel: ${credentials.terminal}` ); } serverStatus = 'connected'; sendServerStatus(); return { success: true, agent: currentAgent, centres: centres }; } return { success: false, message: 'Echec 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 || ''); } // Gerer les cas specifiques 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')) { url = `https://${url}`; } 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 }; }); } function getColorForIndex(index) { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#DDA0DD', '#FFD93D', '#6BCB77']; return colors[index % colors.length]; } // Deconnexion agent via Socket.IO ipcMain.handle('logout', async () => { if (currentAgent && adapter) { try { log('Logoff agent', { accessCode: currentAgent.accessCode }); await adapter.logoff(); console.log('Agent deconnecte du serveur'); log('Agent deconnecte avec succes', { accessCode: currentAgent.accessCode, name: currentAgent.name }); } catch (error) { console.error('Erreur lors de la deconnexion:', error); } } currentAgent = null; currentTerminal = null; agentConnectionInfo = null; if (mainWindow) { mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`); } return { success: true }; }); ipcMain.handle('quit-app', async () => { app.quit(); }); ipcMain.handle('get-current-agent', () => { if (!currentAgent || !agentConnectionInfo) return null; const centres = processApplicationUrls(agentConnectionInfo.connList); return { agent: currentAgent, centres: centres, terminal: currentTerminal }; }); ipcMain.handle('simulate-call', (event, callData) => { mainWindow.webContents.send('incoming-call', callData); return { success: true }; }); ipcMain.handle('get-simulated-calls', () => { if (!config.cti || !config.cti.appelSimules) return []; return config.cti.appelSimules.map(appel => { const centre = config.centres ? config.centres.find(c => c.id === appel.centreId) : null; 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}.json`; const filePath = path.join(notesDir, fileName); let notesData = { agent: currentAgent.id, agentName: currentAgent.name, currentNote: noteData.content, lastModified: new Date().toISOString(), centre: noteData.centre, history: [] }; if (fs.existsSync(filePath)) { try { const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8')); if (existingData.currentNote && existingData.currentNote !== noteData.content) { notesData.history = existingData.history || []; notesData.history.unshift({ content: existingData.currentNote, date: existingData.lastModified, centre: existingData.centre }); 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 }; }); 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; }); 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 []; }); 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() }); history = history.slice(0, 100); fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); return { success: true }; }); ipcMain.handle('is-development', () => { return process.env.NODE_ENV === 'development'; });