Files
SimpleClient-releases/main.js
Pierre Marx a8f167b7c5 refactor: simplification des libellés et titre
- Renommage checkbox "Déconnexion" → "Débloquer" pour plus de clarté
- Simplification du titre de l'application en "SimpleConnect"
- Mise à jour du changelog v1.1.2
2025-09-04 14:41:44 -04:00

529 lines
15 KiB
JavaScript

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'
});
// 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);
// 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');
} catch (logoffError) {
console.warn('Erreur lors de la déconnexion forcée (session peut-être déjà fermée):', logoffError.message);
// Continuer même si la déconnexion échoue - la session est peut-être déjà fermée
}
}
// 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';
});