Files
SimpleClient-releases/renderer.js
Pierre Marx 06b4e2819d feat: Système de persistance des notes amélioré avec fichier unique par agent
- Un seul fichier notes_{agentId}.json par agent (plus d'accumulation)
- Auto-save après 2 secondes d'inactivité
- Restauration automatique au démarrage depuis fichier ou localStorage
- Historique des 50 dernières versions intégré dans le fichier
- Synchronisation transparente fichier/localStorage
- Notifications visuelles lors de la restauration
2025-09-04 16:49:07 -04:00

1240 lines
43 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
};
// Variables pour le redimensionnement des notes
let isResizingNotes = false;
let notesStartWidth = 380;
let notesStartX = 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();
// Les notes seront chargées dans showMainPage()
} else {
// S'assurer que le formulaire est propre au démarrage
resetLoginForm();
// Mettre le focus sur le champ code d'accès
const accessCodeInput = document.getElementById('accessCode');
if (accessCodeInput) {
accessCodeInput.focus();
}
}
// 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 sauvegarder notes
const saveNotesBtn = document.getElementById('saveNotesBtn');
if (saveNotesBtn) {
saveNotesBtn.addEventListener('click', saveNotes);
}
// Bouton effacer notes
const clearNotesBtn = document.getElementById('clearNotesBtn');
if (clearNotesBtn) {
clearNotesBtn.addEventListener('click', clearNotes);
}
// Bouton toggle notes
const toggleNotesBtn = document.getElementById('toggleNotesBtn');
if (toggleNotesBtn) {
toggleNotesBtn.addEventListener('click', toggleNotes);
}
// Bouton fermer notes
const closeNotesBtn = document.getElementById('closeNotesBtn');
if (closeNotesBtn) {
closeNotesBtn.addEventListener('click', hideNotes);
}
// Bouton rafraîchir
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.addEventListener('click', refreshCurrentWebview);
}
// Charger les préférences utilisateur
loadUserPreferences();
// Initialiser le redimensionnement des notes
initNotesResize();
// Gérer les boutons de la modal de déconnexion (si on est sur la page principale)
const cancelLogoutBtn = document.getElementById('cancelLogoutBtn');
const confirmLogoutBtn = document.getElementById('confirmLogoutBtn');
if (cancelLogoutBtn && confirmLogoutBtn) {
// Les event listeners sont gérés dans showLogoutModal()
}
// É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);
// Afficher la modal de progression de connexion
showLoginProgress(forceDisconnect);
// Désactiver le bouton pendant la connexion
loginBtn.disabled = true;
loginBtn.textContent = forceDisconnect ? 'Reconnexion...' : 'Connexion en cours...';
errorDiv.textContent = '';
// Attendre un peu pour que l'animation soit visible
await new Promise(resolve => setTimeout(resolve, 300));
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);
// Mettre à jour le message de progression
updateLoginProgress('Chargement de vos centres...', 'Préparation de l\'interface...');
// Attendre encore un peu pour une transition fluide
await new Promise(resolve => setTimeout(resolve, 500));
// Masquer la modal et afficher la page principale
hideLoginProgress();
showMainPage();
} else {
// Masquer la modal en cas d'échec
hideLoginProgress();
errorDiv.textContent = result.message || 'Identifiants incorrects';
loginBtn.disabled = false;
loginBtn.textContent = 'Se connecter';
}
} catch (error) {
console.error('Erreur lors de la connexion:', error);
// Masquer la modal en cas d'erreur
hideLoginProgress();
errorDiv.textContent = 'Erreur de connexion. Veuillez réessayer.';
loginBtn.disabled = false;
loginBtn.textContent = 'Se connecter';
}
}
// Déconnexion
function handleLogout() {
showLogoutModal();
}
// Afficher la modal de déconnexion
function showLogoutModal() {
const modal = document.getElementById('logoutModal');
modal.classList.add('active');
// Bouton annuler
const cancelBtn = document.getElementById('cancelLogoutBtn');
const confirmBtn = document.getElementById('confirmLogoutBtn');
// Gérer les clics
const handleCancel = () => {
modal.classList.remove('active');
cancelBtn.removeEventListener('click', handleCancel);
confirmBtn.removeEventListener('click', handleConfirm);
};
const handleConfirm = async () => {
// Afficher l'animation de déconnexion
showLogoutProgress();
// Attendre un peu pour que l'animation soit visible
await new Promise(resolve => setTimeout(resolve, 300));
// Effectuer la déconnexion
await ipcRenderer.invoke('logout');
// Attendre encore un peu pour une transition fluide
await new Promise(resolve => setTimeout(resolve, 500));
// Réinitialiser et fermer
modal.classList.remove('active');
hideLogoutProgress();
currentAgent = null;
currentCentres = [];
activeCenter = null;
webviews = {};
resetLoginForm(); // Réinitialiser le formulaire
showLoginPage();
// Forcer le focus après la fermeture complète de la modal
setTimeout(() => {
const accessCodeInput = document.getElementById('accessCode');
if (accessCodeInput) {
accessCodeInput.focus();
accessCodeInput.select();
}
}, 200);
cancelBtn.removeEventListener('click', handleCancel);
confirmBtn.removeEventListener('click', handleConfirm);
};
cancelBtn.addEventListener('click', handleCancel);
confirmBtn.addEventListener('click', handleConfirm);
// Fermer la modal en cliquant en dehors
modal.addEventListener('click', (e) => {
if (e.target === modal) {
handleCancel();
}
});
// Fermer avec Escape
const handleEscape = (e) => {
if (e.key === 'Escape') {
handleCancel();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
// Afficher l'animation de progression de déconnexion
function showLogoutProgress() {
const modal = document.querySelector('.logout-modal');
const icon = document.getElementById('logoutIcon');
const spinner = document.getElementById('logoutSpinner');
const title = document.getElementById('logoutTitle');
const text = document.getElementById('logoutText');
const subtitle = document.getElementById('logoutSubtitle');
const buttons = document.getElementById('logoutButtons');
// Masquer l'icône et afficher le spinner
icon.style.display = 'none';
spinner.style.display = 'block';
// Mettre à jour les textes
title.textContent = 'Déconnexion en cours...';
text.textContent = 'Veuillez patienter';
subtitle.textContent = 'Fermeture de votre session...';
// Masquer les boutons
buttons.style.display = 'none';
}
// Masquer l'animation de progression de déconnexion
function hideLogoutProgress() {
const icon = document.getElementById('logoutIcon');
const spinner = document.getElementById('logoutSpinner');
const title = document.getElementById('logoutTitle');
const text = document.getElementById('logoutText');
const subtitle = document.getElementById('logoutSubtitle');
const buttons = document.getElementById('logoutButtons');
// Restaurer l'état initial
icon.style.display = 'flex';
spinner.style.display = 'none';
title.textContent = 'Déconnexion';
text.textContent = 'Êtes-vous sûr de vouloir vous déconnecter ?';
subtitle.textContent = 'Votre session sera fermée et vous devrez vous reconnecter.';
buttons.style.display = 'flex';
}
// Afficher l'animation de progression de connexion
function showLoginProgress(isForceDisconnect = false) {
const modal = document.getElementById('loginModal');
const title = document.getElementById('loginTitle');
const text = document.getElementById('loginText');
const subtitle = document.getElementById('loginSubtitle');
// Afficher la modal
modal.classList.add('active');
// Utiliser les mêmes textes peu importe le type de connexion
title.textContent = 'Connexion en cours...';
text.textContent = 'Authentification auprès du serveur';
subtitle.textContent = 'Veuillez patienter...';
}
// Mettre à jour le texte de progression de connexion
function updateLoginProgress(text, subtitle) {
const textElement = document.getElementById('loginText');
const subtitleElement = document.getElementById('loginSubtitle');
if (textElement) textElement.textContent = text;
if (subtitleElement) subtitleElement.textContent = subtitle;
}
// Masquer l'animation de progression de connexion
function hideLoginProgress() {
const modal = document.getElementById('loginModal');
modal.classList.remove('active');
}
// === GESTION DES PAGES ===
function showLoginPage() {
document.getElementById('loginPage').classList.add('active');
document.getElementById('mainPage').classList.remove('active');
// Mettre le focus sur le champ code d'accès après un court délai
setTimeout(() => {
const accessCodeInput = document.getElementById('accessCode');
if (accessCodeInput) {
accessCodeInput.focus();
accessCodeInput.select(); // Sélectionner le contenu s'il y en a
}
}, 100);
}
// Réinitialiser le formulaire de connexion
function resetLoginForm() {
// Vider les champs
const accessCode = document.getElementById('accessCode');
const password = document.getElementById('password');
const terminal = document.getElementById('terminal');
const forceDisconnect = document.getElementById('forceDisconnect');
const loginError = document.getElementById('loginError');
const loginBtn = document.querySelector('#loginForm button[type="submit"]');
if (accessCode) accessCode.value = '';
if (password) password.value = '';
// Réinitialiser le terminal (garder la dernière sélection si elle existe)
// Ne pas réinitialiser le terminal pour garder la préférence
// Décocher la checkbox de déblocage
if (forceDisconnect) forceDisconnect.checked = false;
// Vider les messages d'erreur
if (loginError) loginError.textContent = '';
// Réactiver le bouton et remettre le texte par défaut
if (loginBtn) {
loginBtn.disabled = false;
loginBtn.textContent = 'Se connecter';
}
}
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');
// Sélectionner automatiquement le premier centre/onglet (après tri alphabétique)
if (currentCentres.length > 0) {
// Trier pour obtenir le premier alphabétiquement
const sortedCentres = [...currentCentres].sort((a, b) => {
return a.id.localeCompare(b.id, 'fr', { numeric: true });
});
selectCenter(sortedCentres[0].id);
}
// Charger les notes sauvegardées APRÈS que la page soit affichée
setTimeout(() => {
loadSavedNotes();
setupAutoSave();
}, 100);
}
// === GESTION DES CENTRES ===
function initializeCenters() {
const centerTabs = document.getElementById('centerTabs');
const webviewContainer = document.getElementById('webviewContainer');
// Vider les contenus existants
centerTabs.innerHTML = '';
webviewContainer.innerHTML = '';
// Trier les centres par ordre alphabétique du code (id)
const sortedCentres = [...currentCentres].sort((a, b) => {
return a.id.localeCompare(b.id, 'fr', { numeric: true });
});
// Créer les onglets et webviews
sortedCentres.forEach(centre => {
// 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');
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`);
// 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-fail-load', (e) => {
console.error(`Erreur chargement ${centre.nom}:`, e);
});
});
}
// Sélectionner un centre
function selectCenter(centerId) {
// Mettre à jour le centre actif
activeCenter = 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';
});
}
// === 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++;
// 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++;
}
// === 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() {
// Statistiques supprimées de l'interface
console.log('Stats:', { calls: callStats.calls, appointments: callStats.appointments });
}
async function saveNotes() {
const notes = document.getElementById('quickNotes').value;
if (!notes.trim()) {
showNotification('Aucune note à sauvegarder', 'error');
return;
}
// Sauvegarder dans localStorage pour persistance immédiate
localStorage.setItem('currentNotes', notes);
localStorage.setItem('currentNotesDate', new Date().toISOString());
// Sauvegarder aussi dans un fichier pour historique
const result = await ipcRenderer.invoke('save-notes', {
content: notes,
centre: activeCenter
});
if (result.success) {
showNotification('Notes sauvegardées avec succès !', 'success');
// Ajouter à l'historique local
addToNotesHistory(notes, activeCenter);
}
}
// Charger les notes sauvegardées au démarrage
async function loadSavedNotes() {
const textarea = document.getElementById('quickNotes');
if (!textarea) return;
// D'abord essayer de charger depuis le fichier serveur
try {
const serverNotes = await ipcRenderer.invoke('get-notes');
if (serverNotes && serverNotes.currentNote) {
textarea.value = serverNotes.currentNote;
// Sauvegarder aussi dans localStorage pour synchronisation
localStorage.setItem('currentNotes', serverNotes.currentNote);
localStorage.setItem('currentNotesDate', serverNotes.lastModified);
const date = new Date(serverNotes.lastModified);
const formattedDate = date.toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
showNotification(`Notes restaurées (${formattedDate})`, 'info');
console.log(`Notes chargées depuis le serveur (${formattedDate})`);
return;
}
} catch (error) {
console.error('Erreur chargement notes serveur:', error);
}
// Sinon, charger depuis localStorage
const savedNotes = localStorage.getItem('currentNotes');
const savedDate = localStorage.getItem('currentNotesDate');
if (savedNotes) {
textarea.value = savedNotes;
if (savedDate) {
const date = new Date(savedDate);
const formattedDate = date.toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
console.log(`Notes restaurées depuis localStorage (${formattedDate})`);
}
} else {
textarea.value = '';
console.log('Aucune note sauvegardée à restaurer');
}
}
// Ajouter à l'historique des notes
function addToNotesHistory(content, centre) {
let history = JSON.parse(localStorage.getItem('notesHistory') || '[]');
history.unshift({
content: content,
centre: centre,
date: new Date().toISOString(),
id: Date.now()
});
// Garder seulement les 20 dernières notes
history = history.slice(0, 20);
localStorage.setItem('notesHistory', JSON.stringify(history));
}
// Récupérer l'historique des notes
function getNotesHistory() {
return JSON.parse(localStorage.getItem('notesHistory') || '[]');
}
// === 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 DES NOTES DYNAMIQUES ===
function toggleNotes() {
const notesSection = document.getElementById('notesSection');
const toggleBtn = document.getElementById('toggleNotesBtn');
const webviewContainer = document.getElementById('webviewContainer');
if (notesSection.classList.contains('visible')) {
hideNotes();
} else {
showNotes();
}
}
function showNotes() {
const notesSection = document.getElementById('notesSection');
const toggleBtn = document.getElementById('toggleNotesBtn');
const webviewContainer = document.getElementById('webviewContainer');
// Restaurer la largeur sauvegardée
const savedWidth = localStorage.getItem('notesWidth');
if (savedWidth && notesSection) {
const width = parseInt(savedWidth);
if (width >= 280 && width <= 600) {
notesSection.style.width = `${width}px`;
document.documentElement.style.setProperty('--notes-width', `${width}px`);
}
}
// Réinitialiser la position pour l'affichage
if (notesSection) {
notesSection.style.right = '0';
}
notesSection.classList.add('visible');
toggleBtn.classList.add('active');
// Ajuster la largeur de la webview pour faire de la place aux notes
if (webviewContainer) {
webviewContainer.classList.add('notes-open');
}
// Sauvegarder la préférence
localStorage.setItem('notesVisible', 'true');
// Focus sur le textarea avec un délai pour l'animation
setTimeout(() => {
const textarea = document.getElementById('quickNotes');
if (textarea) {
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}
}, 300);
}
function hideNotes() {
const notesSection = document.getElementById('notesSection');
const toggleBtn = document.getElementById('toggleNotesBtn');
const webviewContainer = document.getElementById('webviewContainer');
notesSection.classList.remove('visible');
toggleBtn.classList.remove('active');
// Restaurer la largeur normale de la webview
if (webviewContainer) {
webviewContainer.classList.remove('notes-open');
}
// S'assurer que le panneau est complètement caché
if (notesSection) {
const currentWidth = notesSection.offsetWidth || 380;
notesSection.style.right = `-${currentWidth + 20}px`;
}
// Sauvegarder la préférence
localStorage.setItem('notesVisible', 'false');
}
function clearNotes() {
const textarea = document.getElementById('quickNotes');
if (textarea && confirm('Êtes-vous sûr de vouloir effacer toutes les notes ?')) {
textarea.value = '';
// Effacer aussi de localStorage
localStorage.removeItem('currentNotes');
localStorage.removeItem('currentNotesDate');
textarea.focus();
showNotification('Notes effacées', 'info');
}
}
// Sauvegarder automatiquement les notes en cours de frappe (debounced)
let autoSaveTimeout;
function setupAutoSave() {
const textarea = document.getElementById('quickNotes');
if (textarea) {
textarea.addEventListener('input', () => {
clearTimeout(autoSaveTimeout);
autoSaveTimeout = setTimeout(() => {
const notes = textarea.value;
if (notes.trim()) {
localStorage.setItem('currentNotes', notes);
localStorage.setItem('currentNotesDate', new Date().toISOString());
console.log('Notes auto-sauvegardées');
}
}, 2000); // Sauvegarder après 2 secondes d'inactivité
});
}
}
// === GESTION DES PRÉFÉRENCES UTILISATEUR ===
function loadUserPreferences() {
// Charger l'état des notes
const notesVisible = localStorage.getItem('notesVisible');
if (notesVisible === 'true') {
// On n'affiche pas automatiquement les notes au démarrage
// L'utilisateur devra cliquer pour les afficher
}
// Charger la largeur sauvegardée des notes
const savedNotesWidth = localStorage.getItem('notesWidth');
if (savedNotesWidth) {
const width = parseInt(savedNotesWidth);
if (width >= 280 && width <= 600) {
document.documentElement.style.setProperty('--notes-width', `${width}px`);
const notesSection = document.getElementById('notesSection');
if (notesSection) {
notesSection.style.width = `${width}px`;
}
}
}
// Charger d'autres préférences si nécessaire
const lastTheme = localStorage.getItem('theme');
if (lastTheme) {
document.body.setAttribute('data-theme', lastTheme);
}
}
function saveUserPreferences() {
// Cette fonction peut être étendue pour sauvegarder d'autres préférences
const notesSection = document.getElementById('notesSection');
const preferences = {
notesVisible: notesSection.classList.contains('visible'),
notesWidth: notesSection ? notesSection.offsetWidth : 380,
theme: document.body.getAttribute('data-theme') || 'light'
};
Object.keys(preferences).forEach(key => {
localStorage.setItem(key, preferences[key]);
});
}
// === GESTION DU REDIMENSIONNEMENT DES NOTES ===
function initNotesResize() {
const resizeHandle = document.getElementById('notesResizeHandle');
const notesSection = document.getElementById('notesSection');
const webviewContainer = document.getElementById('webviewContainer');
if (!resizeHandle || !notesSection) return;
let rafId = null;
resizeHandle.addEventListener('mousedown', (e) => {
isResizingNotes = true;
notesStartX = e.clientX;
notesStartWidth = notesSection.offsetWidth;
// Ajouter les classes pour désactiver les transitions
notesSection.classList.add('resizing');
if (webviewContainer) {
webviewContainer.classList.add('resizing');
}
// Empêcher la sélection de texte pendant le redimensionnement
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
// Ajouter un overlay pour capturer tous les mouvements de souris
const overlay = document.createElement('div');
overlay.id = 'resize-overlay';
overlay.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; cursor: col-resize;';
document.body.appendChild(overlay);
e.preventDefault();
});
const handleMouseMove = (e) => {
if (!isResizingNotes) return;
// Utiliser requestAnimationFrame pour la fluidité
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
// Calculer la nouvelle largeur (inversé car le panneau est à droite)
const deltaX = notesStartX - e.clientX;
const newWidth = notesStartWidth + deltaX;
// Appliquer les limites min/max
const clampedWidth = Math.min(Math.max(newWidth, 280), 600);
// Mettre à jour la largeur avec transform pour plus de fluidité
notesSection.style.width = `${clampedWidth}px`;
// Mettre à jour la variable CSS pour la webview
document.documentElement.style.setProperty('--notes-width', `${clampedWidth}px`);
});
};
const handleMouseUp = () => {
if (!isResizingNotes) return;
isResizingNotes = false;
// Annuler l'animation frame en cours
if (rafId) cancelAnimationFrame(rafId);
// Retirer l'overlay
const overlay = document.getElementById('resize-overlay');
if (overlay) overlay.remove();
// Retirer les classes de redimensionnement
const notesSection = document.getElementById('notesSection');
const webviewContainer = document.getElementById('webviewContainer');
if (notesSection) {
notesSection.classList.remove('resizing');
}
if (webviewContainer) {
webviewContainer.classList.remove('resizing');
}
// Réactiver la sélection de texte
document.body.style.userSelect = '';
document.body.style.cursor = '';
// Sauvegarder la nouvelle largeur
if (notesSection) {
localStorage.setItem('notesWidth', notesSection.offsetWidth);
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
// === FONCTION DE RAFRAÎCHISSEMENT ===
function refreshCurrentWebview() {
if (activeCenter && webviews[activeCenter]) {
webviews[activeCenter].reload();
console.log(`Rafraîchissement de la webview ${activeCenter}`);
// Animation visuelle du bouton
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.classList.add('rotating');
setTimeout(() => {
refreshBtn.classList.remove('rotating');
}, 1000);
}
} else {
console.log('Aucune webview active à rafraîchir');
}
}
// === 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';
}
}