connexion avec signalR
This commit is contained in:
249
main.js
249
main.js
@@ -6,8 +6,10 @@ const signalR = require('@microsoft/signalr');
|
|||||||
let mainWindow;
|
let mainWindow;
|
||||||
let config;
|
let config;
|
||||||
let currentAgent = null;
|
let currentAgent = null;
|
||||||
|
let currentTerminal = null; // Terminal téléphonique de l'agent connecté
|
||||||
let signalRConnection = null;
|
let signalRConnection = null;
|
||||||
let signalRStatus = 'disconnected'; // disconnected, connecting, connected, error
|
let signalRStatus = 'disconnected'; // disconnected, connecting, connected, error
|
||||||
|
let agentConnectionInfo = null; // Informations complètes retournées par SignalR
|
||||||
|
|
||||||
// Charger la configuration
|
// Charger la configuration
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
@@ -97,10 +99,70 @@ function initializeSignalR() {
|
|||||||
function setupSignalRMethods() {
|
function setupSignalRMethods() {
|
||||||
// Écouter les événements IPBX
|
// Écouter les événements IPBX
|
||||||
signalRConnection.on('IpbxEvent', (name, args) => {
|
signalRConnection.on('IpbxEvent', (name, args) => {
|
||||||
if (!args) return;
|
if (!args || !agentConnectionInfo) return;
|
||||||
|
|
||||||
const event = args[0];
|
const event = args[0];
|
||||||
console.log('Événement IPBX reçu:', event);
|
console.log('Événement IPBX reçu:', {
|
||||||
// TODO: Gérer les événements d'appel
|
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
|
// 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') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
@@ -208,46 +285,158 @@ ipcMain.handle('get-terminal-list', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connexion agent
|
// Connexion agent via SignalR
|
||||||
ipcMain.handle('login-agent', (event, credentials) => {
|
ipcMain.handle('login-agent', async (event, credentials) => {
|
||||||
const agent = config.agents.find(a =>
|
// Vérifier que SignalR est connecté
|
||||||
a.email === credentials.email &&
|
if (!signalRConnection || signalRStatus !== 'connected') {
|
||||||
a.password === credentials.password
|
return {
|
||||||
);
|
success: false,
|
||||||
|
message: 'Connexion au serveur SignalR non établie. Veuillez réessayer.'
|
||||||
if (agent) {
|
};
|
||||||
currentAgent = agent;
|
}
|
||||||
// Retourner l'agent avec ses centres assignés
|
|
||||||
const centresAssignes = config.centres.filter(c =>
|
try {
|
||||||
agent.centresAssignes.includes(c.id)
|
console.log('Tentative de connexion agent:', credentials.email, 'Terminal:', credentials.terminal);
|
||||||
);
|
|
||||||
return {
|
// Appel SignalR pour l'authentification
|
||||||
success: true,
|
const result = await signalRConnection.invoke('AgentLogin',
|
||||||
agent: agent,
|
credentials.email,
|
||||||
centres: centresAssignes
|
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
|
// Traiter les URLs des applications avec les placeholders
|
||||||
ipcMain.handle('logout', () => {
|
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;
|
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 };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Obtenir l'agent actuel
|
// Obtenir l'agent actuel
|
||||||
ipcMain.handle('get-current-agent', () => {
|
ipcMain.handle('get-current-agent', () => {
|
||||||
if (!currentAgent) return null;
|
if (!currentAgent || !agentConnectionInfo) return null;
|
||||||
|
|
||||||
const centresAssignes = config.centres.filter(c =>
|
// Retourner les centres traités depuis SignalR
|
||||||
currentAgent.centresAssignes.includes(c.id)
|
const centres = processApplicationUrls(agentConnectionInfo.connList);
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
agent: currentAgent,
|
agent: currentAgent,
|
||||||
centres: centresAssignes
|
centres: centres,
|
||||||
|
terminal: currentTerminal
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
153
renderer.js
153
renderer.js
@@ -65,9 +65,41 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
ipcRenderer.on('incoming-call', (event, callData) => {
|
ipcRenderer.on('incoming-call', (event, callData) => {
|
||||||
handleIncomingCall(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) {
|
async function handleLogin(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -75,6 +107,7 @@ async function handleLogin(e) {
|
|||||||
const password = document.getElementById('password').value;
|
const password = document.getElementById('password').value;
|
||||||
const terminal = document.getElementById('terminal').value;
|
const terminal = document.getElementById('terminal').value;
|
||||||
const errorDiv = document.getElementById('loginError');
|
const errorDiv = document.getElementById('loginError');
|
||||||
|
const loginBtn = document.querySelector('#loginForm button[type="submit"]');
|
||||||
|
|
||||||
// Vérifier que le terminal est sélectionné et valide
|
// Vérifier que le terminal est sélectionné et valide
|
||||||
if (!terminal) {
|
if (!terminal) {
|
||||||
@@ -91,24 +124,38 @@ async function handleLogin(e) {
|
|||||||
// Sauvegarder le terminal sélectionné pour la prochaine fois
|
// Sauvegarder le terminal sélectionné pour la prochaine fois
|
||||||
localStorage.setItem('last-terminal', terminal);
|
localStorage.setItem('last-terminal', terminal);
|
||||||
|
|
||||||
// Pour l'instant, utiliser l'authentification locale (simulation)
|
// Désactiver le bouton pendant la connexion
|
||||||
// TODO: Intégrer l'authentification SignalR
|
loginBtn.disabled = true;
|
||||||
const config = await ipcRenderer.invoke('get-config');
|
loginBtn.textContent = 'Connexion en cours...';
|
||||||
const agent = config.agents.find(a =>
|
errorDiv.textContent = '';
|
||||||
a.email === `${accessCode}@callcenter.fr` &&
|
|
||||||
a.password === password
|
|
||||||
);
|
|
||||||
|
|
||||||
if (agent) {
|
try {
|
||||||
currentAgent = agent;
|
// Préparer les credentials pour SignalR
|
||||||
currentAgent.terminal = terminal; // Ajouter le terminal sélectionné
|
const credentials = {
|
||||||
currentCentres = config.centres.filter(c =>
|
email: accessCode, // Utiliser directement le code agent comme email
|
||||||
agent.centresAssignes.includes(c.id)
|
password: password,
|
||||||
);
|
terminal: terminal
|
||||||
errorDiv.textContent = '';
|
};
|
||||||
showMainPage();
|
|
||||||
} else {
|
// Appeler l'authentification SignalR
|
||||||
errorDiv.textContent = 'Code d\'accès ou mot de passe incorrect';
|
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);
|
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() {
|
async function saveNotes() {
|
||||||
const notes = document.getElementById('quickNotes').value;
|
const notes = document.getElementById('quickNotes').value;
|
||||||
if (!notes.trim()) return;
|
if (!notes.trim()) return;
|
||||||
|
|||||||
35
styles.css
35
styles.css
@@ -13,6 +13,41 @@ body {
|
|||||||
overflow: hidden;
|
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 === */
|
/* === PAGES === */
|
||||||
.page {
|
.page {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user