From 6069a7238be7bb7d8e3304201b0fe6d1f14e5350 Mon Sep 17 00:00:00 2001 From: Pierre Marx Date: Thu, 4 Sep 2025 14:18:36 -0400 Subject: [PATCH] connexion avec signalR --- main.js | 249 +++++++++++++++++++++++++++++++++++++++++++++------- renderer.js | 153 ++++++++++++++++++++++++++++---- styles.css | 35 ++++++++ 3 files changed, 389 insertions(+), 48 deletions(-) diff --git a/main.js b/main.js index 6219e64..571afef 100644 --- a/main.js +++ b/main.js @@ -6,8 +6,10 @@ 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() { @@ -97,10 +99,70 @@ function initializeSignalR() { function setupSignalRMethods() { // Écouter les événements IPBX signalRConnection.on('IpbxEvent', (name, args) => { - if (!args) return; + if (!args || !agentConnectionInfo) return; + const event = args[0]; - console.log('Événement IPBX reçu:', event); - // TODO: Gérer les événements d'appel + 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' }); } @@ -159,7 +221,22 @@ app.whenReady().then(() => { }); // Quitter quand toutes les fenêtres sont fermées -app.on('window-all-closed', () => { +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(); } @@ -208,46 +285,158 @@ ipcMain.handle('get-terminal-list', async () => { } }); -// Connexion agent -ipcMain.handle('login-agent', (event, credentials) => { - const agent = config.agents.find(a => - a.email === credentials.email && - a.password === credentials.password - ); - - if (agent) { - currentAgent = agent; - // Retourner l'agent avec ses centres assignés - const centresAssignes = config.centres.filter(c => - agent.centresAssignes.includes(c.id) - ); - return { - success: true, - agent: agent, - centres: centresAssignes +// 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' }; } - - return { success: false, message: 'Email ou mot de passe incorrect' }; }); -// Déconnexion -ipcMain.handle('logout', () => { +// 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) return null; + if (!currentAgent || !agentConnectionInfo) return null; - const centresAssignes = config.centres.filter(c => - currentAgent.centresAssignes.includes(c.id) - ); + // Retourner les centres traités depuis SignalR + const centres = processApplicationUrls(agentConnectionInfo.connList); return { agent: currentAgent, - centres: centresAssignes + centres: centres, + terminal: currentTerminal }; }); diff --git a/renderer.js b/renderer.js index 36befb7..99f2061 100644 --- a/renderer.js +++ b/renderer.js @@ -65,9 +65,41 @@ document.addEventListener('DOMContentLoaded', async () => { ipcRenderer.on('incoming-call', (event, callData) => { handleIncomingCall(callData); }); + + // Écouter les événements SignalR de basculement de centre + ipcRenderer.on('switch-to-center', (event, data) => { + console.log('Basculement vers le centre:', data.centreName); + + // Trouver le centre et basculer automatiquement + const centre = currentCentres.find(c => c.id === data.centreId); + if (centre) { + selectCenter(data.centreId); + + // Afficher une notification + showNotification(`Appel entrant sur ${data.centreName}`, 'info'); + + // Mettre à jour le statut + updateAgentStatus('EN APPEL'); + } + }); + + // Écouter la libération de centre après raccrochage + ipcRenderer.on('release-center', (event, data) => { + console.log('Libération de la file:', data.queueName); + + // Mettre à jour le statut + updateAgentStatus('DISPONIBLE'); + + // Incrementer le compteur d'appels + callStats.calls++; + updateCallStats(); + + // Afficher une notification + showNotification('Appel terminé', 'success'); + }); }); -// Connexion +// Connexion via SignalR async function handleLogin(e) { e.preventDefault(); @@ -75,6 +107,7 @@ async function handleLogin(e) { const password = document.getElementById('password').value; const terminal = document.getElementById('terminal').value; const errorDiv = document.getElementById('loginError'); + const loginBtn = document.querySelector('#loginForm button[type="submit"]'); // Vérifier que le terminal est sélectionné et valide if (!terminal) { @@ -91,24 +124,38 @@ async function handleLogin(e) { // Sauvegarder le terminal sélectionné pour la prochaine fois localStorage.setItem('last-terminal', terminal); - // Pour l'instant, utiliser l'authentification locale (simulation) - // TODO: Intégrer l'authentification SignalR - const config = await ipcRenderer.invoke('get-config'); - const agent = config.agents.find(a => - a.email === `${accessCode}@callcenter.fr` && - a.password === password - ); + // Désactiver le bouton pendant la connexion + loginBtn.disabled = true; + loginBtn.textContent = 'Connexion en cours...'; + errorDiv.textContent = ''; - if (agent) { - currentAgent = agent; - currentAgent.terminal = terminal; // Ajouter le terminal sélectionné - currentCentres = config.centres.filter(c => - agent.centresAssignes.includes(c.id) - ); - errorDiv.textContent = ''; - showMainPage(); - } else { - errorDiv.textContent = 'Code d\'accès ou mot de passe incorrect'; + try { + // Préparer les credentials pour SignalR + const credentials = { + email: accessCode, // Utiliser directement le code agent comme email + password: password, + terminal: terminal + }; + + // Appeler l'authentification SignalR + const result = await ipcRenderer.invoke('login-agent', credentials); + + if (result.success) { + currentAgent = result.agent; + currentCentres = result.centres; + errorDiv.textContent = ''; + console.log('Connexion réussie:', currentAgent.name, 'sur le poste', terminal); + showMainPage(); + } else { + errorDiv.textContent = result.message || 'Identifiants incorrects'; + loginBtn.disabled = false; + loginBtn.textContent = 'Se connecter'; + } + } catch (error) { + console.error('Erreur lors de la connexion:', error); + errorDiv.textContent = 'Erreur de connexion. Veuillez réessayer.'; + loginBtn.disabled = false; + loginBtn.textContent = 'Se connecter'; } } @@ -421,6 +468,76 @@ function playNotificationSound() { oscillator.stop(audioContext.currentTime + 0.2); } +// Fonction pour mettre à jour le statut de l'agent +function updateAgentStatus(status) { + const statusElement = document.getElementById('statusText'); + const indicatorElement = document.getElementById('statusIndicator'); + + if (statusElement && indicatorElement) { + switch(status) { + case 'DISPONIBLE': + updateStatus('available'); + break; + case 'EN APPEL': + updateStatus('incall'); + break; + case 'HORS LIGNE': + updateStatus('offline'); + break; + default: + statusElement.textContent = status; + } + } +} + +// Fonction pour afficher une notification +function showNotification(message, type = 'info') { + // Créer un élément de notification temporaire + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.textContent = message; + + // Styles de base pour la notification + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'}; + color: white; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + z-index: 10000; + animation: slideIn 0.3s ease; + `; + + document.body.appendChild(notification); + + // Retirer la notification après 3 secondes + setTimeout(() => { + notification.style.animation = 'slideOut 0.3s ease'; + setTimeout(() => notification.remove(), 300); + }, 3000); + + // Jouer un son si c'est une notification d'appel + if (type === 'info' && message.includes('Appel entrant')) { + playNotificationSound(); + } +} + +// Fonction pour mettre à jour les statistiques d'appels +function updateCallStats() { + const callCountElement = document.getElementById('callCount'); + const appointmentCountElement = document.getElementById('appointmentCount'); + + if (callCountElement) { + callCountElement.textContent = callStats.calls; + } + if (appointmentCountElement) { + appointmentCountElement.textContent = callStats.appointments; + } +} + async function saveNotes() { const notes = document.getElementById('quickNotes').value; if (!notes.trim()) return; diff --git a/styles.css b/styles.css index 4018fca..5091363 100644 --- a/styles.css +++ b/styles.css @@ -13,6 +13,41 @@ body { overflow: hidden; } +/* === ANIMATIONS === */ +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(102, 126, 234, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(102, 126, 234, 0); + } +} + /* === PAGES === */ .page { display: none;