import React, { useState, useEffect } from 'react'; import { Users, Download, Calendar, Clock, FileText, Filter, LogOut, RefreshCw, AlertCircle } from 'lucide-react'; import { useAuth } from '../context/AuthContext'; interface TimeEntry { id: string; formateur: string; campus: string; date: string; type: 'preparation' | 'correction'; hours: number; description: string; status: 'pending' | 'approved' | 'rejected'; heure_debut?: string; heure_fin?: string; formateur_numero?: number; formateur_email?: string; // NOUVEAU CHAMP } interface FormateurAvecDeclarations { userPrincipalName: string; displayName: string; nom: string; prenom: string; campus: string; poste: string; nbDeclarations: number; displayText: string; } interface SystemStatus { operatingMode: string; hasFormateurEmailColumn: boolean; canAccessFormateurView: boolean; } const RHDashboard: React.FC = () => { const [selectedCampus, setSelectedCampus] = useState('all'); const [selectedMonth, setSelectedMonth] = useState(() => { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); return `${year}-${month}`; }); const { logout } = useAuth(); const [selectedFormateur, setSelectedFormateur] = useState('all'); const [timeEntries, setTimeEntries] = useState([]); const [formateursAvecDeclarations, setFormateursAvecDeclarations] = useState([]); const [loading, setLoading] = useState(true); const [loadingFormateurs, setLoadingFormateurs] = useState(false); const [error, setError] = useState(''); const [systemStatus, setSystemStatus] = useState(null); // Fonction pour normaliser/mapper les noms de campus const normalizeCampus = (campus: string): string => { const mapping: { [key: string]: string } = { // Codes courts vers noms longs 'MRS': 'Marseille', 'mrs': 'Marseille', 'NTE': 'Nantes', 'nte': 'Nantes', 'CGY': 'Cergy', 'cgy': 'Cergy', 'SQY': 'SQY', 'sqy': 'SQY', 'ensqy': 'SQY', 'SQY/CGY': 'SQY/CGY', // Campus multi-sites 'sqy/cgy': 'SQY/CGY', // Noms complets (au cas où) 'Marseille': 'Marseille', 'Nantes': 'Nantes', 'Cergy': 'Cergy', 'Saint-Quentin-en-Yvelines': 'SQY', 'MARSEILLE': 'Marseille', 'NANTES': 'Nantes', 'CERGY': 'Cergy', // Fallback 'Non défini': 'Non défini', '': 'Non défini' }; return mapping[campus] || campus || 'Non défini'; }; // Fonction inversée pour obtenir le code depuis le label const getCampusCode = (label: string): string => { const reverseMapping: { [key: string]: string } = { 'Marseille': 'MRS', 'Nantes': 'NTE', 'Cergy': 'CGY', 'SQY': 'SQY', 'SQY/CGY': 'SQY/CGY' }; return reverseMapping[label] || label; }; // Liste des campus pour l'interface (incluant SQY/CGY comme cas spécial) const campuses = ['Cergy', 'Nantes', 'SQY', 'Marseille', 'SQY/CGY']; // Fonction pour générer le hash (même logique que le backend - pour compatibilité) const generateHashFromEmail = (email: string): number => { let hash = 0; for (let i = 0; i < email.length; i++) { const char = email.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash) % 10000 + 1000; }; // Fonction pour récupérer le statut du système const loadSystemStatus = async () => { try { const response = await fetch('http://localhost:3002/api/diagnostic'); if (response.ok) { const data = await response.json(); setSystemStatus(data.systemStatus); console.log('📊 Statut système:', data.systemStatus); } } catch (error) { console.error('Erreur chargement statut système:', error); } }; // Fonction améliorée pour associer les déclarations avec les formateurs const getFormateurInfo = (declaration: any): { nom: string, prenom: string, campus: string, displayText: string } => { console.log('🔍 Recherche formateur pour:', declaration); // NOUVEAU SYSTÈME : Si on a un email, chercher le formateur correspondant if (declaration.formateur_email || declaration.formateur_email_fk) { const email = declaration.formateur_email || declaration.formateur_email_fk; const formateurTrouve = formateursAvecDeclarations.find(f => f.userPrincipalName === email ); if (formateurTrouve) { console.log(`✅ Formateur trouvé par email: ${email} -> ${formateurTrouve.displayText} (${formateurTrouve.campus})`); return { nom: formateurTrouve.nom, prenom: formateurTrouve.prenom, campus: formateurTrouve.campus, // Garder le campus original de la vue displayText: formateurTrouve.displayText }; } } // FALLBACK : Si on a les champs nom/prenom/campus directement dans la déclaration (nouveau système) if (declaration.nom && declaration.nom !== 'undefined') { console.log(`✅ Formateur trouvé dans les données déclaration: ${declaration.nom} ${declaration.prenom} (${declaration.campus})`); return { nom: declaration.nom, prenom: declaration.prenom || '', campus: declaration.campus || declaration.Campus || 'Non défini', displayText: declaration.formateur_nom_complet || `${declaration.nom} ${declaration.prenom || ''}`.trim() }; } // ANCIEN SYSTÈME : Si on a un formateur_numero, essayer de le correspondre if (declaration.formateur_numero && formateursAvecDeclarations.length > 0) { console.log(`🔍 Recherche par hash pour numéro: ${declaration.formateur_numero}`); // Méthode 1: Chercher par hash d'email const formateurParHash = formateursAvecDeclarations.find(f => { if (f.userPrincipalName) { const hash = generateHashFromEmail(f.userPrincipalName); return hash === declaration.formateur_numero; } return false; }); if (formateurParHash) { console.log(`✅ Formateur trouvé par hash: ${declaration.formateur_numero} -> ${formateurParHash.displayText} (${formateurParHash.campus})`); return { nom: formateurParHash.nom, prenom: formateurParHash.prenom, campus: formateurParHash.campus, displayText: formateurParHash.displayText }; } } // DERNIER RECOURS : Mapping connu pour les cas difficiles const knownMappings: { [key: number]: { nom: string, prenom: string, campus: string } } = { 122: { nom: 'Admin', prenom: 'Ensup', campus: 'SQY' }, 999: { nom: 'Inconnu', prenom: 'Formateur', campus: 'Non défini' } }; if (declaration.formateur_numero && knownMappings[declaration.formateur_numero]) { const known = knownMappings[declaration.formateur_numero]; console.log(`✅ Mapping connu trouvé: ${declaration.formateur_numero} -> ${known.nom} (${known.campus})`); return { nom: known.nom, prenom: known.prenom, campus: known.campus, displayText: `${known.nom} ${known.prenom} (${known.campus})` }; } // FALLBACK FINAL const identifier = declaration.formateur_email || declaration.formateur_numero || 'Inconnu'; console.log(`⚠️ Aucune correspondance trouvée, utilisation du fallback pour: ${identifier}`); return { nom: `Formateur ${identifier}`, prenom: '', campus: 'Non défini', displayText: `Formateur ${identifier} (Non défini)` }; }; // Fonction pour charger TOUS les formateurs (avec et sans déclarations) const loadFormateursAvecDeclarations = async () => { try { setLoadingFormateurs(true); setError(''); console.log('🔄 Chargement de tous les formateurs...'); // Essayer d'abord de récupérer tous les formateurs depuis la vue const response = await fetch('http://localhost:3002/api/formateurs-vue'); if (response.ok) { const data = await response.json(); if (data.success) { console.log(`✅ ${data.formateurs.length} formateurs chargés depuis la vue`); console.log('📊 Mode serveur:', data.mode); // Convertir le format et initialiser à 0 déclarations const tousLesFormateurs = data.formateurs.map((f: any) => ({ ...f, nbDeclarations: 0, poste: f.poste || '' })); setFormateursAvecDeclarations(tousLesFormateurs); } else { throw new Error(data.message || 'Erreur lors du chargement des formateurs'); } } else { throw new Error(`Erreur HTTP ${response.status}`); } } catch (error: any) { console.error('❌ Erreur chargement formateurs depuis la vue:', error); // Fallback: essayer avec formateurs-avec-declarations (ancienne méthode) try { console.log('🔄 Tentative de fallback avec formateurs-avec-declarations...'); const fallbackResponse = await fetch('http://localhost:3002/api/formateurs-avec-declarations'); if (fallbackResponse.ok) { const fallbackData = await fallbackResponse.json(); if (fallbackData.success) { setFormateursAvecDeclarations(fallbackData.formateurs); console.log('🔄 Fallback réussi:', fallbackData.formateurs.length, 'formateurs avec déclarations'); setError(''); // Effacer l'erreur si le fallback fonctionne } } } catch (fallbackError) { console.error('❌ Fallback échoué aussi:', fallbackError); setError(`Impossible de charger les formateurs: ${error.message}`); } } finally { setLoadingFormateurs(false); } }; // Charger les déclarations depuis votre API const loadDeclarations = async () => { try { setLoading(true); setError(''); console.log('🔄 Chargement des déclarations...'); const response = await fetch('http://localhost:3002/api/get_declarations'); if (!response.ok) { throw new Error(`Erreur HTTP ${response.status} lors du chargement des déclarations`); } const data = await response.json(); console.log('📊 Données déclarations reçues:', data.length); console.log('📊 Exemple de déclaration:', data[0]); // Convertir les données (avec normalisation des campus) const convertedEntries: TimeEntry[] = data.map((d: any) => { const formateurInfo = getFormateurInfo(d); return { id: d.id.toString(), formateur: formateurInfo.displayText, campus: normalizeCampus(formateurInfo.campus), // Normaliser le campus date: d.date.split('T')[0], type: d.activityType, hours: d.duree, description: d.description || '', status: d.status || 'pending', heure_debut: d.heure_debut || null, heure_fin: d.heure_fin || null, formateur_numero: d.formateur_numero, formateur_email: d.formateur_email || d.formateur_email_fk }; }); setTimeEntries(convertedEntries); console.log(`✅ ${convertedEntries.length} déclarations traitées`); // Log des formateurs uniques pour debug const formateursUniques = [...new Set(convertedEntries.map(e => e.formateur))]; console.log(`📊 ${formateursUniques.length} formateurs uniques dans les déclarations:`, formateursUniques.slice(0, 5)); } catch (err: any) { console.error('❌ Erreur lors du chargement des déclarations:', err); setError(err.message); } finally { setLoading(false); } }; // Charger les données au démarrage useEffect(() => { const loadData = async () => { await loadSystemStatus(); await loadFormateursAvecDeclarations(); await loadDeclarations(); // Debug: Afficher tous les campus uniques trouvés dans les données setTimeout(() => { const campusUniques = [...new Set(formateursAvecDeclarations.map(f => f.campus))].filter(Boolean); const campusDeclarations = [...new Set(timeEntries.map(e => e.campus))].filter(Boolean); console.log('🏢 Campus trouvés dans les formateurs:', campusUniques); console.log('🏢 Campus trouvés dans les déclarations:', campusDeclarations); console.log('🏢 Tous les campus uniques:', [...new Set([...campusUniques, ...campusDeclarations])]); }, 2000); }; loadData(); }, []); // Re-traiter les déclarations quand les formateurs sont chargés ET calculer les nombres de déclarations useEffect(() => { if (formateursAvecDeclarations.length > 0) { console.log('🔄 Re-traitement des déclarations avec les nouveaux formateurs...'); // Si on a déjà des déclarations, les retraiter if (timeEntries.length > 0) { const updatedEntries = timeEntries.map(entry => { const originalDeclaration = { formateur_numero: entry.formateur_numero, formateur_email: entry.formateur_email }; const formateurInfo = getFormateurInfo(originalDeclaration); return { ...entry, formateur: formateurInfo.displayText, campus: formateurInfo.campus }; }); setTimeEntries(updatedEntries); } // Calculer le nombre de déclarations pour chaque formateur const formateursAvecCompte = formateursAvecDeclarations.map(formateur => { const nbDeclarations = timeEntries.filter(entry => { // Correspondance par email if (entry.formateur_email === formateur.userPrincipalName) { return true; } // Correspondance par hash if (entry.formateur_numero && formateur.userPrincipalName) { const hash = generateHashFromEmail(formateur.userPrincipalName); return hash === entry.formateur_numero; } // Correspondance par nom affiché const expectedDisplayText = `${formateur.nom} ${formateur.prenom} (${formateur.campus})`.trim(); return entry.formateur === expectedDisplayText || entry.formateur === formateur.displayText; }).length; return { ...formateur, nbDeclarations }; }); setFormateursAvecDeclarations(formateursAvecCompte); } }, [timeEntries.length]); // Déclenché quand le nombre de déclarations change // Réinitialiser le filtre formateur quand on change de campus useEffect(() => { // Si un formateur est sélectionné et qu'on change de campus if (selectedFormateur !== 'all') { // Vérifier si le formateur sélectionné existe encore dans le campus filtré const formateurExists = formateursAvecDeclarations.some(formateur => { const campusMatch = selectedCampus === 'all' || formateur.campus === selectedCampus; return campusMatch && formateur.displayText === selectedFormateur; }); // Si le formateur n'existe pas dans le nouveau campus, le réinitialiser if (!formateurExists) { console.log(`🔄 Réinitialisation du filtre formateur (${selectedFormateur} n'est pas dans ${selectedCampus})`); setSelectedFormateur('all'); } } }, [selectedCampus, formateursAvecDeclarations]); // Fonction de déconnexion const handleLogout = () => { localStorage.removeItem('token'); localStorage.removeItem('o365_token'); localStorage.removeItem('user'); logout(); window.location.href = '/login'; }; // Fonction de rafraîchissement complète const handleRefresh = async () => { await loadSystemStatus(); await loadFormateursAvecDeclarations(); await loadDeclarations(); }; // Filtrage des données (avec normalisation des campus) const filteredEntries = timeEntries.filter(entry => { const entryNormalizedCampus = normalizeCampus(entry.campus); const campusMatch = selectedCampus === 'all' || entryNormalizedCampus === selectedCampus; const formateurMatch = selectedFormateur === 'all' || entry.formateur === selectedFormateur; const monthMatch = entry.date.startsWith(selectedMonth); return campusMatch && formateurMatch && monthMatch; }); // Générer la liste des formateurs filtrés par campus (avec déduplication) const formateursUniques = formateursAvecDeclarations .filter(formateur => { // Si "tous les campus" est sélectionné, afficher tous les formateurs if (selectedCampus === 'all') return true; // Normaliser le campus du formateur et comparer avec le campus sélectionné const formateurCampusNormalized = normalizeCampus(formateur.campus); return formateurCampusNormalized === selectedCampus; }) .reduce((acc: any[], formateur) => { // Déduplicquer par email (userPrincipalName) const existing = acc.find(f => f.userPrincipalName === formateur.userPrincipalName); if (!existing) { acc.push({ displayText: formateur.displayText, nbDeclarations: formateur.nbDeclarations || 0, userPrincipalName: formateur.userPrincipalName, campus: normalizeCampus(formateur.campus) // Normaliser le campus pour l'affichage }); } else { // Si le formateur existe déjà, additionner les déclarations existing.nbDeclarations += (formateur.nbDeclarations || 0); } return acc; }, []) .sort((a, b) => a.displayText.localeCompare(b.displayText)); // Statistiques par campus (avec normalisation) const getStatsForCampus = (campus: string) => { const campusEntries = timeEntries.filter(entry => { const entryNormalizedCampus = normalizeCampus(entry.campus); return entryNormalizedCampus === campus; }); const totalHours = campusEntries.reduce((sum, entry) => sum + entry.hours, 0); const preparationHours = campusEntries .filter(entry => entry.type === 'preparation') .reduce((sum, entry) => sum + entry.hours, 0); const correctionHours = campusEntries .filter(entry => entry.type === 'correction') .reduce((sum, entry) => sum + entry.hours, 0); return { totalHours, preparationHours, correctionHours }; }; const handleExport = () => { const csvHeaders = ['Date', 'Formateur', 'Campus', 'Type', 'Heures', 'Heure Début', 'Heure Fin', 'Description']; const csvData = filteredEntries.map(entry => [ new Date(entry.date).toLocaleDateString('fr-FR'), entry.formateur || 'Non défini', entry.campus || 'Non défini', entry.type === 'preparation' ? 'Préparation' : 'Correction', entry.hours.toString(), entry.heure_debut || 'Non défini', entry.heure_fin || 'Non défini', (entry.description || '').replace(/[\r\n]+/g, ' ').trim() ]); // Fonction pour nettoyer les valeurs (enlever guillemets et point-virgules problématiques) const cleanCsvValue = (value) => { return String(value || '') .replace(/"/g, '""') // Doubler les guillemets .replace(/;/g, ','); // Remplacer ; par , dans les données }; // Utiliser le point-virgule comme séparateur pour Excel français const csvContent = '\uFEFF' + [csvHeaders, ...csvData] .map(row => row.map(cell => cleanCsvValue(cell)).join(';')) // Point-virgule ici ! .join('\r\n'); // Utiliser \r\n pour Windows const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.href = url; link.download = `declarations_heures_${selectedMonth}.csv`; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => { URL.revokeObjectURL(url); }, 100); }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' }); }; const formatTime = (timeString: string | null | undefined) => { if (!timeString) return '-'; try { if (timeString.match(/^\d{2}:\d{2}$/)) { return timeString; } if (timeString.match(/^\d{2}:\d{2}:\d{2}\.\d+$/)) { return timeString.substring(0, 5); } if (timeString.match(/^\d{2}:\d{2}:\d{2}$/)) { return timeString.substring(0, 5); } if (timeString.includes('T')) { const timePart = timeString.split('T')[1]; if (timePart) { const hourMin = timePart.substring(0, 5); if (hourMin.match(/^\d{2}:\d{2}$/)) { return hourMin; } } } return timeString; } catch (error) { return '-'; } }; return (
{/* Header */}

Vue RH - GTF

Gestion et suivi des déclarations des formateurs

{/* Message d'erreur */} {error && (
Erreur de chargement

{error}

)} {/* Indicateur de chargement */} {(loading || loadingFormateurs) && (

{loadingFormateurs ? 'Chargement des formateurs...' : 'Chargement des déclarations...'}

)} {!loading && !loadingFormateurs && ( <> {/* Filtres */}

Filtres

setSelectedMonth(e.target.value)} className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
{/* Statistiques par campus */}
{campuses.map(campus => { const stats = getStatsForCampus(campus); return (

{campus}

Total: {stats.totalHours}h
Préparation: {stats.preparationHours}h
Correction: {stats.correctionHours}h
); })}
{/* Résumé des résultats */}

Résultats filtrés

{filteredEntries.length} déclaration(s) trouvée(s)

{filteredEntries.reduce((sum, entry) => sum + entry.hours, 0)}h

Total des heures

{/* Tableau des déclarations */}

Déclarations d'heures

{filteredEntries.length === 0 ? ( ) : ( filteredEntries.map((entry) => ( )) )}
Date Formateur Campus Type Heures Heure Début Heure Fin Description
{timeEntries.length === 0 ? "Aucune déclaration dans la base de données" : "Aucune déclaration trouvée pour les critères sélectionnés" }
{formatDate(entry.date)} {entry.formateur} {entry.campus}
{entry.type === 'preparation' ? ( ) : ( )} {entry.type === 'preparation' ? 'Préparation' : 'Correction'}
{entry.hours}h
{formatTime(entry.heure_debut)}
{formatTime(entry.heure_fin)}
{entry.description}
)}
); }; export default RHDashboard;