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 v${app.getVersion()}`,
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');
// Forcer le titre après le chargement de la page (le
HTML l'écrase sinon)
mainWindow.webContents.on('did-finish-load', () => {
mainWindow.setTitle(`SimpleConnect v${app.getVersion()}`);
});
// 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,
isSignalR: connectionInfo.isSignalR,
isRestSocketIO: connectionInfo.isRestSocketIO,
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);
}
}
// Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage
// Le serveur .NET ne supporte pas ce message
// On laisse la connexion se fermer naturellement avec l'application
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 la version de l'application
ipcMain.handle('get-app-version', () => {
return app.getVersion();
});
// 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 v${app.getVersion()} - 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 v${app.getVersion()}`);
}
return { success: true };
});
// Handler pour quitter l'application proprement
ipcMain.handle('quit-app', async () => {
// Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage
// Le serveur .NET ne supporte pas ce message
// On laisse la connexion se fermer naturellement avec l'application
// (comme le fait le client de prod)
// 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';
});