Size

18.8 KB

Version

1.1.1

Created

Nov 17, 2025

Updated

25 days ago

1// ==UserScript==
2// @name		Extracteur Responsables Relations Entreprises
3// @description		Scrape automatiquement les noms des responsables relations entreprises sur les sites d'écoles
4// @version		1.1.1
5// @match		https://*.edu/*
6// @match		https://*.ac.*/*
7// @match		https://*.univ-*/*
8// @match		https://*.university/*
9// @match		https://*.universite/*
10// @match		https://*.college/*
11// @match		https://*.school/*
12// @match		https://*.ecole/*
13// @match		https://*.enseignement/*
14// @match		https://*.education/*
15// @match		https://*.campus/*
16// @match		https://*.iut.*/*
17// @match		https://*.iae.*/*
18// @match		https://*.escp.eu/*
19// @match		https://*.essec.edu/*
20// @match		https://*.hec.edu/*
21// @match		https://*.edhec.edu/*
22// @match		https://*.em-lyon.com/*
23// @match		https://*.skema.edu/*
24// @match		https://*.kedge.edu/*
25// @match		https://*.neoma-bs.fr/*
26// @match		https://*.grenoble-em.com/*
27// @match		https://*.tbs-education.fr/*
28// @match		https://*.audencia.com/*
29// @match		https://*.rennes-sb.fr/*
30// @match		https://*.bsb-education.com/*
31// @match		https://*.montpellier-bs.com/*
32// @match		https://*.excelia-group.com/*
33// @match		https://*.inseec.com/*
34// @match		https://*.psl.eu/*
35// @match		https://*.polytechnique.edu/*
36// @match		https://*.centralesupelec.fr/*
37// @match		https://*.mines-paristech.fr/*
38// @match		https://*.ensae.fr/*
39// @match		https://*.sciencespo.fr/*
40// @match		https://*.dauphine.psl.eu/*
41// @match		https://*.vatel.fr/*
42// @match		https://*.ferrandi-paris.fr/*
43// @match		https://*.paul-bocuse.com/*
44// @match		https://*.ieseg.fr/*
45// @match		https://*.esce.fr/*
46// @match		https://*.istec.fr/*
47// @match		https://*.ipag.edu/*
48// @match		https://*.ppa.fr/*
49// @match		https://*.ebs-paris.fr/*
50// @match		https://*.emlv.fr/*
51// @match		https://*.icd-ecoles.com/*
52// @match		https://*.efap.com/*
53// @match		https://*.sup-de-pub.com/*
54// @match		https://*.iscom.fr/*
55// @match		https://*.iscpa.fr/*
56// @match		https://*.epitech.eu/*
57// @match		https://*.epita.fr/*
58// @match		https://*.42.fr/*
59// @match		https://*.supinfo.com/*
60// @match		https://*.hetic.net/*
61// @match		https://*.gobelins.fr/*
62// @match		https://*.ensad.fr/*
63// @match		https://*.ensci.com/*
64// @match		https://*.strate.design/*
65// @match		https://*.edhec.com/*
66// @match		https://*.ieseg.fr/*
67// @icon		https://www.vatel.fr/assets/images/favicon.ico
68// @grant		GM.getValue
69// @grant		GM.setValue
70// @grant		GM.xmlhttpRequest
71// ==/UserScript==
72(function() {
73    'use strict';
74
75    // Configuration
76    const CONFIG = {
77        buttonText: '🔍 Extraire Responsables RE',
78        storageKey: 'responsables_relations_entreprises',
79        keywords: ['relations entreprises', 'corporate relations', 'entreprise', 'partenariats', 'partnerships', 'business relations']
80    };
81
82    // Fonction pour créer le bouton d'extraction
83    function createExtractButton() {
84        const button = document.createElement('button');
85        button.id = 'extract-responsables-btn';
86        button.textContent = CONFIG.buttonText;
87        button.style.cssText = `
88            position: fixed;
89            top: 20px;
90            right: 20px;
91            z-index: 10000;
92            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
93            color: white;
94            border: none;
95            padding: 12px 24px;
96            border-radius: 8px;
97            font-size: 14px;
98            font-weight: 600;
99            cursor: pointer;
100            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
101            transition: all 0.3s ease;
102        `;
103        
104        button.addEventListener('mouseenter', () => {
105            button.style.transform = 'translateY(-2px)';
106            button.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)';
107        });
108        
109        button.addEventListener('mouseleave', () => {
110            button.style.transform = 'translateY(0)';
111            button.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
112        });
113        
114        button.addEventListener('click', extractResponsables);
115        document.body.appendChild(button);
116        console.log('Bouton d\'extraction créé');
117    }
118
119    // Fonction pour afficher un indicateur de chargement
120    function showLoadingIndicator() {
121        const loader = document.createElement('div');
122        loader.id = 'extraction-loader';
123        loader.innerHTML = `
124            <div style="display: flex; flex-direction: column; align-items: center; gap: 10px;">
125                <div class="spinner"></div>
126                <div>Extraction en cours...</div>
127            </div>
128        `;
129        loader.style.cssText = `
130            position: fixed;
131            top: 80px;
132            right: 20px;
133            z-index: 10000;
134            background: white;
135            color: #333;
136            padding: 20px;
137            border-radius: 8px;
138            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
139            font-size: 14px;
140        `;
141        
142        const style = document.createElement('style');
143        style.textContent = `
144            .spinner {
145                border: 3px solid #f3f3f3;
146                border-top: 3px solid #667eea;
147                border-radius: 50%;
148                width: 30px;
149                height: 30px;
150                animation: spin 1s linear infinite;
151            }
152            @keyframes spin {
153                0% { transform: rotate(0deg); }
154                100% { transform: rotate(360deg); }
155            }
156        `;
157        document.head.appendChild(style);
158        document.body.appendChild(loader);
159    }
160
161    // Fonction pour masquer l'indicateur de chargement
162    function hideLoadingIndicator() {
163        const loader = document.getElementById('extraction-loader');
164        if (loader) loader.remove();
165    }
166
167    // Fonction pour extraire le contenu de la page
168    function getPageContent() {
169        // Récupérer le texte visible de la page
170        const bodyText = document.body.innerText;
171        
172        // Récupérer aussi les sections spécifiques qui pourraient contenir des contacts
173        const contactSections = document.querySelectorAll('[class*="contact"], [class*="team"], [class*="staff"], [id*="contact"], [id*="team"]');
174        let sectionsText = '';
175        contactSections.forEach(section => {
176            sectionsText += section.innerText + '\n';
177        });
178        
179        return {
180            url: window.location.href,
181            title: document.title,
182            bodyText: bodyText.substring(0, 5000), // Limiter pour ne pas surcharger
183            sectionsText: sectionsText.substring(0, 3000)
184        };
185    }
186
187    // Fonction principale d'extraction
188    async function extractResponsables() {
189        console.log('Début de l\'extraction des responsables relations entreprises');
190        showLoadingIndicator();
191        
192        try {
193            const pageContent = getPageContent();
194            console.log('Contenu de la page récupéré:', pageContent.url);
195            
196            // Utiliser l'API RM.aiCall pour extraire les informations
197            const prompt = `Analyse le contenu suivant d'un site web d'école et extrait UNIQUEMENT les noms et informations des responsables relations entreprises, partenariats entreprises, ou corporate relations.
198
199URL: ${pageContent.url}
200Titre: ${pageContent.title}
201
202Contenu:
203${pageContent.sectionsText || pageContent.bodyText}
204
205Cherche spécifiquement les personnes avec des titres comme:
206- Responsable relations entreprises
207- Chargé(e) de relations entreprises
208- Corporate relations manager
209- Responsable partenariats
210- Business relations
211- Développement entreprises
212
213Si tu ne trouves AUCUNE personne correspondant à ces critères, retourne un tableau vide.`;
214
215            const result = await RM.aiCall(prompt, {
216                type: 'json_schema',
217                json_schema: {
218                    name: 'responsables_extraction',
219                    schema: {
220                        type: 'object',
221                        properties: {
222                            responsables: {
223                                type: 'array',
224                                items: {
225                                    type: 'object',
226                                    properties: {
227                                        nom: { type: 'string' },
228                                        prenom: { type: 'string' },
229                                        titre: { type: 'string' },
230                                        email: { type: 'string' },
231                                        telephone: { type: 'string' },
232                                        campus: { type: 'string' }
233                                    },
234                                    required: ['nom']
235                                }
236                            },
237                            ecole: { type: 'string' },
238                            url: { type: 'string' }
239                        },
240                        required: ['responsables', 'ecole', 'url']
241                    }
242                }
243            });
244            
245            console.log('Résultat de l\'extraction:', result);
246            
247            // Sauvegarder les résultats
248            await saveResults(result);
249            
250            // Afficher les résultats
251            displayResults(result);
252            
253        } catch (error) {
254            console.error('Erreur lors de l\'extraction:', error);
255            showError('Erreur lors de l\'extraction: ' + error.message);
256        } finally {
257            hideLoadingIndicator();
258        }
259    }
260
261    // Fonction pour sauvegarder les résultats
262    async function saveResults(result) {
263        try {
264            // Récupérer les résultats existants
265            const existingData = await GM.getValue(CONFIG.storageKey, '[]');
266            const allResults = JSON.parse(existingData);
267            
268            // Ajouter les nouveaux résultats avec timestamp
269            const newEntry = {
270                ...result,
271                timestamp: new Date().toISOString(),
272                extractedAt: new Date().toLocaleString('fr-FR')
273            };
274            
275            // Vérifier si cette URL existe déjà et la remplacer
276            const existingIndex = allResults.findIndex(r => r.url === result.url);
277            if (existingIndex >= 0) {
278                allResults[existingIndex] = newEntry;
279            } else {
280                allResults.push(newEntry);
281            }
282            
283            await GM.setValue(CONFIG.storageKey, JSON.stringify(allResults));
284            console.log('Résultats sauvegardés');
285        } catch (error) {
286            console.error('Erreur lors de la sauvegarde:', error);
287        }
288    }
289
290    // Fonction pour afficher les résultats
291    function displayResults(result) {
292        // Supprimer l'ancien panneau s'il existe
293        const oldPanel = document.getElementById('results-panel');
294        if (oldPanel) oldPanel.remove();
295        
296        const panel = document.createElement('div');
297        panel.id = 'results-panel';
298        panel.style.cssText = `
299            position: fixed;
300            top: 80px;
301            right: 20px;
302            z-index: 10000;
303            background: white;
304            color: #333;
305            padding: 20px;
306            border-radius: 8px;
307            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
308            max-width: 400px;
309            max-height: 500px;
310            overflow-y: auto;
311            font-family: Arial, sans-serif;
312        `;
313        
314        let html = `
315            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
316                <h3 style="margin: 0; color: #667eea;">Résultats de l'extraction</h3>
317                <button id="close-panel" style="background: none; border: none; font-size: 20px; cursor: pointer; color: #999;">×</button>
318            </div>
319            <div style="margin-bottom: 10px;">
320                <strong>École:</strong> ${result.ecole || 'Non identifiée'}
321            </div>
322            <div style="margin-bottom: 15px;">
323                <strong>URL:</strong> <a href="${result.url}" target="_blank" style="color: #667eea; font-size: 12px; word-break: break-all;">${result.url}</a>
324            </div>
325        `;
326        
327        if (result.responsables && result.responsables.length > 0) {
328            html += `<div style="margin-bottom: 10px;"><strong>Responsables trouvés: ${result.responsables.length}</strong></div>`;
329            
330            result.responsables.forEach((resp, index) => {
331                html += `
332                    <div style="background: #f5f5f5; padding: 12px; border-radius: 6px; margin-bottom: 10px; border-left: 3px solid #667eea;">
333                        <div style="font-weight: 600; color: #333; margin-bottom: 5px;">
334                            ${resp.prenom || ''} ${resp.nom}
335                        </div>
336                        ${resp.titre ? `<div style="color: #667eea; font-size: 13px; margin-bottom: 5px;">${resp.titre}</div>` : ''}
337                        ${resp.campus ? `<div style="font-size: 12px; color: #666;">📍 ${resp.campus}</div>` : ''}
338                        ${resp.email ? `<div style="font-size: 12px; color: #666;">📧 ${resp.email}</div>` : ''}
339                        ${resp.telephone ? `<div style="font-size: 12px; color: #666;">📞 ${resp.telephone}</div>` : ''}
340                    </div>
341                `;
342            });
343            
344            html += `
345                <button id="copy-results" style="
346                    width: 100%;
347                    background: #667eea;
348                    color: white;
349                    border: none;
350                    padding: 10px;
351                    border-radius: 6px;
352                    cursor: pointer;
353                    font-weight: 600;
354                    margin-top: 10px;
355                ">📋 Copier les résultats</button>
356                <button id="export-all" style="
357                    width: 100%;
358                    background: #764ba2;
359                    color: white;
360                    border: none;
361                    padding: 10px;
362                    border-radius: 6px;
363                    cursor: pointer;
364                    font-weight: 600;
365                    margin-top: 10px;
366                ">💾 Exporter tous les résultats</button>
367            `;
368        } else {
369            html += `
370                <div style="background: #fff3cd; padding: 12px; border-radius: 6px; color: #856404; border-left: 3px solid #ffc107;">
371                    ⚠️ Aucun responsable relations entreprises trouvé sur cette page.
372                </div>
373            `;
374        }
375        
376        panel.innerHTML = html;
377        document.body.appendChild(panel);
378        
379        // Ajouter les événements
380        document.getElementById('close-panel').addEventListener('click', () => panel.remove());
381        
382        const copyBtn = document.getElementById('copy-results');
383        if (copyBtn) {
384            copyBtn.addEventListener('click', () => copyResultsToClipboard(result));
385        }
386        
387        const exportBtn = document.getElementById('export-all');
388        if (exportBtn) {
389            exportBtn.addEventListener('click', exportAllResults);
390        }
391    }
392
393    // Fonction pour copier les résultats dans le presse-papiers
394    async function copyResultsToClipboard(result) {
395        let text = `École: ${result.ecole}\nURL: ${result.url}\n\nResponsables Relations Entreprises:\n\n`;
396        
397        result.responsables.forEach((resp, index) => {
398            text += `${index + 1}. ${resp.prenom || ''} ${resp.nom}\n`;
399            if (resp.titre) text += `   Titre: ${resp.titre}\n`;
400            if (resp.campus) text += `   Campus: ${resp.campus}\n`;
401            if (resp.email) text += `   Email: ${resp.email}\n`;
402            if (resp.telephone) text += `   Téléphone: ${resp.telephone}\n`;
403            text += '\n';
404        });
405        
406        try {
407            await GM.setClipboard(text);
408            showNotification('✅ Résultats copiés dans le presse-papiers !');
409        } catch (error) {
410            console.error('Erreur lors de la copie:', error);
411        }
412    }
413
414    // Fonction pour exporter tous les résultats
415    async function exportAllResults() {
416        try {
417            const allData = await GM.getValue(CONFIG.storageKey, '[]');
418            const results = JSON.parse(allData);
419            
420            // Créer un CSV
421            let csv = 'École,URL,Prénom,Nom,Titre,Campus,Email,Téléphone,Date extraction\n';
422            
423            results.forEach(result => {
424                result.responsables.forEach(resp => {
425                    csv += `"${result.ecole || ''}","${result.url}","${resp.prenom || ''}","${resp.nom}","${resp.titre || ''}","${resp.campus || ''}","${resp.email || ''}","${resp.telephone || ''}","${result.extractedAt || ''}"\n`;
426                });
427            });
428            
429            // Télécharger le fichier
430            const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
431            const link = document.createElement('a');
432            link.href = URL.createObjectURL(blob);
433            link.download = `responsables_relations_entreprises_${new Date().toISOString().split('T')[0]}.csv`;
434            link.click();
435            
436            showNotification('✅ Export CSV téléchargé !');
437        } catch (error) {
438            console.error('Erreur lors de l\'export:', error);
439            showError('Erreur lors de l\'export');
440        }
441    }
442
443    // Fonction pour afficher une notification
444    function showNotification(message) {
445        const notif = document.createElement('div');
446        notif.textContent = message;
447        notif.style.cssText = `
448            position: fixed;
449            top: 20px;
450            left: 50%;
451            transform: translateX(-50%);
452            z-index: 10001;
453            background: #4caf50;
454            color: white;
455            padding: 12px 24px;
456            border-radius: 6px;
457            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
458            font-weight: 600;
459        `;
460        document.body.appendChild(notif);
461        
462        setTimeout(() => notif.remove(), 3000);
463    }
464
465    // Fonction pour afficher une erreur
466    function showError(message) {
467        const error = document.createElement('div');
468        error.textContent = message;
469        error.style.cssText = `
470            position: fixed;
471            top: 80px;
472            right: 20px;
473            z-index: 10000;
474            background: #f44336;
475            color: white;
476            padding: 15px;
477            border-radius: 8px;
478            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
479            max-width: 300px;
480        `;
481        document.body.appendChild(error);
482        
483        setTimeout(() => error.remove(), 5000);
484    }
485
486    // Initialisation
487    function init() {
488        console.log('Extension Extracteur Responsables Relations Entreprises initialisée');
489        
490        // Attendre que le DOM soit prêt
491        if (document.readyState === 'loading') {
492            document.addEventListener('DOMContentLoaded', createExtractButton);
493        } else {
494            createExtractButton();
495        }
496    }
497
498    // Démarrer l'extension
499    init();
500})();