Files
SimpleClient-releases/renderer.js
Pierre Marx 2bf8c3605a feat: Affichage du code client dans les onglets des plannings
Les titres des onglets affichent maintenant le code client (centre.id)
au lieu du nom de la file d'attente (centre.nom) pour une identification
plus directe et claire du client concerné.
2025-09-04 14:53:19 -04:00

738 lines
26 KiB
JavaScript

const { ipcRenderer } = require('electron');
// Choices est maintenant chargé via CDN dans index.html
// Variables globales
let currentAgent = null;
let currentCentres = [];
let activeCenter = null;
let webviews = {};
let callStats = {
calls: 0,
appointments: 0
};
// === GESTION DE LA CONNEXION ===
document.addEventListener('DOMContentLoaded', async () => {
// Initialiser l'indicateur SignalR
// Écouter les changements de statut SignalR
ipcRenderer.on('signalr-status', (event, status) => {
updateSignalRIndicator(status);
// Recharger les terminaux à chaque changement de statut
loadTerminals();
});
// Obtenir le statut initial SignalR
const initialStatus = await ipcRenderer.invoke('get-signalr-status');
updateSignalRIndicator(initialStatus);
// Charger immédiatement les terminaux pour la page de login
await loadTerminals();
// Vérifier si un agent est déjà connecté
const agentData = await ipcRenderer.invoke('get-current-agent');
if (agentData) {
currentAgent = agentData.agent;
currentCentres = agentData.centres;
showMainPage();
}
// Gestionnaire du formulaire de connexion
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', handleLogin);
}
// Bouton de déconnexion
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', handleLogout);
}
// Bouton simulation d'appel
const simulateBtn = document.getElementById('simulateCallBtn');
if (simulateBtn) {
simulateBtn.addEventListener('click', showCallSimulation);
}
// Bouton sauvegarder notes
const saveNotesBtn = document.getElementById('saveNotesBtn');
if (saveNotesBtn) {
saveNotesBtn.addEventListener('click', saveNotes);
}
// Écouter les appels entrants
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 via SignalR
async function handleLogin(e) {
e.preventDefault();
const accessCode = document.getElementById('accessCode').value;
const password = document.getElementById('password').value;
const terminal = document.getElementById('terminal').value;
const forceDisconnect = document.getElementById('forceDisconnect').checked;
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) {
errorDiv.textContent = 'Veuillez sélectionner un poste téléphonique';
return;
}
// Valider que le terminal existe dans la liste
if (!validateTerminal(terminal)) {
errorDiv.textContent = 'Poste téléphonique invalide. Veuillez choisir un poste de la liste.';
return;
}
// Sauvegarder le terminal sélectionné pour la prochaine fois
localStorage.setItem('last-terminal', terminal);
// Désactiver le bouton pendant la connexion
loginBtn.disabled = true;
loginBtn.textContent = forceDisconnect ? 'Reconnexion...' : 'Connexion en cours...';
errorDiv.textContent = '';
try {
// Préparer les credentials pour SignalR
const credentials = {
email: accessCode, // Utiliser directement le code agent comme email
password: password,
terminal: terminal,
forceDisconnect: forceDisconnect // Ajouter l'option de déconnexion forcée
};
// 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';
}
}
// Déconnexion
async function handleLogout() {
if (confirm('Voulez-vous vraiment vous déconnecter ?')) {
await ipcRenderer.invoke('logout');
currentAgent = null;
currentCentres = [];
activeCenter = null;
webviews = {};
showLoginPage();
}
}
// === GESTION DES PAGES ===
function showLoginPage() {
document.getElementById('loginPage').classList.add('active');
document.getElementById('mainPage').classList.remove('active');
}
function showMainPage() {
document.getElementById('loginPage').classList.remove('active');
document.getElementById('mainPage').classList.add('active');
// Afficher le nom de l'agent
document.getElementById('agentName').textContent = currentAgent.name;
// Initialiser l'interface
initializeCenters();
updateStatus('available');
}
// === GESTION DES CENTRES ===
function initializeCenters() {
const centersList = document.getElementById('centersList');
const centerTabs = document.getElementById('centerTabs');
const webviewContainer = document.getElementById('webviewContainer');
// Vider les contenus existants
centersList.innerHTML = '';
centerTabs.innerHTML = '';
webviewContainer.innerHTML = '';
// Créer la liste des centres et les onglets
currentCentres.forEach(centre => {
// Élément dans la sidebar
const centerItem = document.createElement('div');
centerItem.className = 'center-item';
centerItem.dataset.centerId = centre.id;
centerItem.innerHTML = `
<div class="center-indicator" style="background-color: ${centre.couleur}"></div>
<div class="center-info">
<div class="center-name">${centre.nom}</div>
<div class="center-phone">${centre.telephone}</div>
</div>
<div class="center-status" id="status-${centre.id}">●</div>
`;
centerItem.addEventListener('click', () => selectCenter(centre.id));
centersList.appendChild(centerItem);
// Onglet
const tab = document.createElement('div');
tab.className = 'tab';
tab.dataset.centerId = centre.id;
tab.style.borderBottomColor = centre.couleur;
tab.textContent = centre.id; // Afficher le code du client au lieu du nom
tab.addEventListener('click', () => selectCenter(centre.id));
centerTabs.appendChild(tab);
// Créer la webview
const webviewWrapper = document.createElement('div');
webviewWrapper.className = 'webview-wrapper';
webviewWrapper.dataset.centerId = centre.id;
webviewWrapper.style.display = 'none';
const webview = document.createElement('webview');
webview.id = `webview-${centre.id}`;
webview.src = centre.url;
webview.className = 'planning-webview';
webview.setAttribute('partition', `persist:${centre.id}`);
webview.setAttribute('useragent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
// Barre d'outils pour la webview
const toolbar = document.createElement('div');
toolbar.className = 'webview-toolbar';
toolbar.innerHTML = `
<button onclick="navigateWebview('${centre.id}', 'back')">◀</button>
<button onclick="navigateWebview('${centre.id}', 'forward')">▶</button>
<button onclick="navigateWebview('${centre.id}', 'reload')">🔄</button>
<span class="webview-url" id="url-${centre.id}">${centre.url}</span>
`;
webviewWrapper.appendChild(toolbar);
webviewWrapper.appendChild(webview);
webviewContainer.appendChild(webviewWrapper);
// Stocker la référence de la webview
webviews[centre.id] = webview;
// Gérer les événements de la webview
webview.addEventListener('dom-ready', () => {
console.log(`Webview ${centre.nom} prête`);
document.getElementById(`status-${centre.id}`).style.color = '#4CAF50';
// Auto-connexion si credentials disponibles
if (centre.credentials && centre.credentials.username) {
// Injecter le script de connexion automatique (à adapter selon le site)
const loginScript = `
// Script générique de connexion - à adapter selon le planning
console.log('Tentative de connexion automatique pour ${centre.nom}');
`;
webview.executeJavaScript(loginScript);
}
});
webview.addEventListener('did-navigate', (e) => {
document.getElementById(`url-${centre.id}`).textContent = e.url;
});
webview.addEventListener('did-fail-load', (e) => {
console.error(`Erreur chargement ${centre.nom}:`, e);
document.getElementById(`status-${centre.id}`).style.color = '#FF5252';
});
});
}
// Sélectionner un centre
function selectCenter(centerId) {
// Mettre à jour le centre actif
activeCenter = centerId;
// Mettre à jour l'UI
document.querySelectorAll('.center-item').forEach(item => {
item.classList.toggle('active', item.dataset.centerId === centerId);
});
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.centerId === centerId);
});
// Afficher la bonne webview
document.querySelectorAll('.webview-wrapper').forEach(wrapper => {
wrapper.style.display = wrapper.dataset.centerId === centerId ? 'flex' : 'none';
});
}
// Navigation dans les webviews
window.navigateWebview = function(centerId, action) {
const webview = webviews[centerId];
if (webview) {
switch(action) {
case 'back':
webview.goBack();
break;
case 'forward':
webview.goForward();
break;
case 'reload':
webview.reload();
break;
}
}
};
// === GESTION DES APPELS ===
function handleIncomingCall(callData) {
const centre = currentCentres.find(c => c.id === callData.centreId);
if (!centre) return;
// Afficher l'alerte
const alert = document.getElementById('incomingCallAlert');
alert.classList.add('active');
document.getElementById('callCenterName').textContent = centre.nom;
document.getElementById('callPatientInfo').textContent =
`${callData.nom || 'Patient'} - ${callData.numero}`;
// Jouer un son (à implémenter)
playNotificationSound();
// Gérer l'acceptation de l'appel
const acceptBtn = document.getElementById('acceptCallBtn');
acceptBtn.onclick = () => {
acceptCall(callData, centre);
};
// Auto-accepter après 3 secondes en mode démo
setTimeout(() => {
if (alert.classList.contains('active')) {
acceptCall(callData, centre);
}
}, 3000);
}
function acceptCall(callData, centre) {
// Masquer l'alerte
document.getElementById('incomingCallAlert').classList.remove('active');
// Sélectionner automatiquement le bon centre
selectCenter(centre.id);
// Mettre à jour le statut
updateStatus('incall', centre.nom);
// Incrémenter les stats
callStats.calls++;
document.getElementById('callCount').textContent = callStats.calls;
// Sauvegarder l'appel dans l'historique
ipcRenderer.invoke('save-call-history', {
...callData,
centreId: centre.id,
centreName: centre.nom,
status: 'answered'
});
// Simuler la fin de l'appel après 30 secondes
setTimeout(() => {
endCall();
}, 30000);
}
function endCall() {
updateStatus('available');
callStats.appointments++;
document.getElementById('appointmentCount').textContent = callStats.appointments;
}
// === SIMULATION D'APPELS ===
function showCallSimulation() {
const modal = document.getElementById('callSimulationModal');
modal.style.display = 'block';
// Charger les appels simulés
loadSimulatedCalls();
// Fermer la modal
modal.querySelector('.close').onclick = () => {
modal.style.display = 'none';
};
// Appel personnalisé
document.getElementById('customCallBtn').onclick = () => {
const customCall = {
numero: prompt('Numéro de téléphone:'),
nom: prompt('Nom du patient:'),
centreId: currentCentres[0]?.id
};
if (customCall.numero) {
ipcRenderer.invoke('simulate-call', customCall);
modal.style.display = 'none';
}
};
}
async function loadSimulatedCalls() {
const calls = await ipcRenderer.invoke('get-simulated-calls');
const listDiv = document.getElementById('simulatedCallsList');
listDiv.innerHTML = calls.map(call => `
<div class="simulated-call-item" onclick="simulateThisCall('${JSON.stringify(call).replace(/'/g, "\\'")}')">
<div class="call-name">${call.nom}</div>
<div class="call-details">${call.numero} - ${call.centreNom}</div>
</div>
`).join('');
}
window.simulateThisCall = function(callDataStr) {
const callData = JSON.parse(callDataStr);
ipcRenderer.invoke('simulate-call', callData);
document.getElementById('callSimulationModal').style.display = 'none';
};
// === UTILITAIRES ===
function updateStatus(status, details = '') {
const indicator = document.getElementById('statusIndicator');
const text = document.getElementById('statusText');
switch(status) {
case 'available':
indicator.className = 'status-indicator available';
text.textContent = 'Disponible';
break;
case 'incall':
indicator.className = 'status-indicator busy';
text.textContent = details ? `En appel - ${details}` : 'En appel';
break;
case 'offline':
indicator.className = 'status-indicator offline';
text.textContent = 'Hors ligne';
break;
}
}
function playNotificationSound() {
// Créer un bip simple avec l'API Web Audio
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'sine';
gainNode.gain.value = 0.3;
oscillator.start();
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;
const result = await ipcRenderer.invoke('save-notes', {
content: notes,
centre: activeCenter
});
if (result.success) {
alert('Notes sauvegardées !');
document.getElementById('quickNotes').value = '';
}
}
// === GESTION DES TERMINAUX ===
let availableTerminals = []; // Stocker les terminaux disponibles
let terminalChoices = null; // Instance Choices.js
async function loadTerminals() {
const terminalSelect = document.getElementById('terminal');
if (!terminalSelect) {
console.log('Element terminal non trouvé');
return;
}
console.log('Chargement des terminaux...');
try {
// Récupérer les terminaux depuis le serveur SignalR
const terminals = await ipcRenderer.invoke('get-terminal-list');
availableTerminals = terminals || [];
console.log(`${terminals.length} terminaux récupérés`);
// Préparer les options pour Choices.js
const choicesData = [];
if (terminals && terminals.length > 0) {
// Afficher tous les terminaux sans groupement
terminals.forEach(terminal => {
choicesData.push({
value: terminal.toString(),
label: `Poste ${terminal}`
});
});
}
// Détruire l'instance existante si elle existe
if (terminalChoices) {
terminalChoices.destroy();
terminalChoices = null;
}
// Vider le select avant d'initialiser Choices
terminalSelect.innerHTML = '';
// Attendre que Choices soit disponible (chargé via CDN)
if (typeof window.Choices === 'undefined') {
console.log('Choices.js pas encore chargé, utilisation du select natif');
// Fallback : utiliser le select natif
terminalSelect.innerHTML = '<option value="">Sélectionner un poste...</option>';
if (terminals && terminals.length > 0) {
terminals.forEach(terminal => {
const option = document.createElement('option');
option.value = terminal;
option.textContent = `Poste ${terminal}`;
terminalSelect.appendChild(option);
});
}
// Pas de retry automatique pour éviter une boucle infinie
return;
}
console.log('Initialisation de Choices.js avec', choicesData.length, 'options');
try {
// Créer une nouvelle instance Choices.js
terminalChoices = new window.Choices(terminalSelect, {
searchEnabled: true,
searchPlaceholderValue: 'Rechercher un poste...',
itemSelectText: '',
noResultsText: 'Aucun poste trouvé',
noChoicesText: 'Aucun poste disponible',
shouldSort: false,
searchResultLimit: 20,
renderChoiceLimit: -1,
placeholder: true,
placeholderValue: 'Sélectionner un poste téléphonique',
choices: choicesData.length > 0 ? choicesData : [{value: '', label: 'Aucun poste disponible', disabled: true}],
searchFields: ['label', 'value'],
fuseOptions: {
includeScore: true,
threshold: 0.3
},
classNames: {
containerOuter: ['choices', 'choices-terminal'],
containerInner: 'choices__inner',
input: 'choices__input',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemChoice: 'choices__item--choice',
placeholder: 'choices__placeholder',
group: 'choices__group',
groupHeading: 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
hiddenState: 'is-hidden',
flippedState: 'is-flipped',
loadingState: 'is-loading',
noResults: 'has-no-results',
noChoices: 'has-no-choices'
}
});
console.log('Choices.js initialisé avec succès');
// Restaurer la dernière sélection si disponible
const lastTerminal = localStorage.getItem('last-terminal');
if (lastTerminal && availableTerminals.map(t => t.toString()).includes(lastTerminal)) {
terminalChoices.setChoiceByValue(lastTerminal.toString());
}
} catch (choicesError) {
console.error('Erreur lors de l\'initialisation de Choices.js:', choicesError);
console.error('Stack:', choicesError.stack);
// En cas d'erreur, utiliser le select natif
terminalSelect.innerHTML = '<option value="">Erreur de chargement - voir console</option>';
if (terminals && terminals.length > 0) {
terminals.forEach(terminal => {
const option = document.createElement('option');
option.value = terminal;
option.textContent = `Poste ${terminal}`;
terminalSelect.appendChild(option);
});
}
}
} catch (error) {
console.error('Erreur générale chargement des terminaux:', error);
// En cas d'erreur, utiliser le select natif
const terminalSelect = document.getElementById('terminal');
if (terminalSelect) {
terminalSelect.innerHTML = '<option value="">Erreur de chargement</option>';
}
}
}
// Valider que le terminal saisi existe dans la liste
function validateTerminal(terminal) {
return availableTerminals.map(t => t.toString()).includes(terminal.toString());
}
// === GESTION INDICATEUR SIGNALR ===
function updateSignalRIndicator(status) {
const indicator = document.getElementById('signalrIndicator');
const text = document.getElementById('signalrText');
if (!indicator || !text) return;
// Réinitialiser les classes
indicator.className = 'signalr-indicator';
switch(status) {
case 'connected':
indicator.classList.add('connected');
text.textContent = 'Connecté au serveur';
break;
case 'connecting':
indicator.classList.add('connecting');
text.textContent = 'Connexion en cours...';
break;
case 'disconnected':
indicator.classList.add('disconnected');
text.textContent = 'Serveur déconnecté';
break;
case 'error':
indicator.classList.add('error');
text.textContent = 'Erreur de connexion';
break;
case 'disabled':
indicator.classList.add('disabled');
text.textContent = 'SignalR désactivé';
break;
default:
text.textContent = 'État inconnu';
}
}