diff --git a/public/team-block-script.js b/public/team-block-script.js index 7d5c5c3..25d315d 100644 --- a/public/team-block-script.js +++ b/public/team-block-script.js @@ -69,7 +69,31 @@ '.legend{display:flex;gap:20px;margin:20px 0;flex-wrap:wrap}' + '.legend-item{display:flex;align-items:center;gap:8px}' + '.legend-color{width:20px;height:20px;border-radius:4px}' + - '.loading{text-align:center;padding:40px;color:#4ade80}'; + '.loading{text-align:center;padding:40px;color:#4ade80}' + + '.clickable{cursor:pointer;transition:all 0.2s}' + + '.clickable:hover{transform:scale(1.02);box-shadow:0 4px 12px rgba(74,222,128,0.3)}' + + '.profile-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:9999;animation:fadeIn 0.2s}' + + '@keyframes fadeIn{from{opacity:0}to{opacity:1}}' + + '.profile-card{background:#1a4d3a;border:2px solid #4ade80;border-radius:12px;padding:25px;max-width:500px;width:90%;max-height:85vh;overflow-y:auto;position:relative;box-shadow:0 20px 60px rgba(0,0,0,0.5)}' + + '.profile-card .close-btn{position:absolute;top:15px;right:15px;background:none;border:none;color:#ff6b6b;font-size:28px;cursor:pointer;line-height:1}' + + '.profile-card .close-btn:hover{color:#ff4444}' + + '.profile-card h2{color:#4ade80;margin-bottom:5px;font-size:24px}' + + '.profile-card .role{color:#a0a0a0;font-size:14px;margin-bottom:15px}' + + '.profile-card .stats{display:flex;gap:15px;flex-wrap:wrap;margin-bottom:20px}' + + '.profile-card .stat{background:rgba(74,222,128,0.15);padding:8px 12px;border-radius:6px;font-size:13px}' + + '.profile-card .stat strong{color:#4ade80}' + + '.profile-card h3{color:#4ade80;font-size:16px;margin:15px 0 10px;border-bottom:1px solid rgba(74,222,128,0.3);padding-bottom:5px}' + + '.profile-card .skills{display:flex;flex-wrap:wrap;gap:8px}' + + '.profile-card .skill-tag{padding:5px 10px;border-radius:4px;font-size:12px;background:rgba(74,222,128,0.2);border:1px solid rgba(74,222,128,0.4)}' + + '.profile-card .skill-tag.expert{background:rgba(74,222,128,0.4);border-color:#4ade80}' + + '.profile-card .skill-tag.intermediate{background:rgba(57,151,212,0.3);border-color:#3997d4}' + + '.profile-card .skill-tag.beginner{background:rgba(245,179,54,0.2);border-color:#f5b336}' + + '.profile-card .interests{display:flex;flex-wrap:wrap;gap:6px}' + + '.profile-card .interest{background:rgba(136,255,136,0.15);padding:4px 8px;border-radius:4px;font-size:11px;color:#88ff88}' + + '.profile-card .projects{list-style:none;padding:0}' + + '.profile-card .projects li{padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.1);font-size:13px}' + + '.profile-card .projects li:last-child{border-bottom:none}' + + '.profile-card .bio{color:#c0c0c0;font-size:13px;line-height:1.5;margin-top:15px;font-style:italic}'; // HTML de la page var html = '
' + @@ -153,6 +177,9 @@ var data = await response.json(); console.log('EQUIPE: Donnees chargees:', Object.keys(data)); + // Stocker les profils pour acces global + window.__memberProfiles = data.memberProfiles || {}; + // Initialiser les visualisations initNetwork(data); initCongestion(data); @@ -229,6 +256,24 @@ animationDuration: 800 } }); + + // Ajouter evenement de clic sur les membres + window.networkCy.on('tap', 'node[type="member"]', function(evt) { + var node = evt.target; + var memberId = node.data('id').replace('member-', ''); + if (window.__memberProfiles && window.__memberProfiles[memberId]) { + showMemberProfile(memberId, window.__memberProfiles[memberId]); + } + }); + + // Style curseur pour les membres + window.networkCy.on('mouseover', 'node[type="member"]', function() { + document.body.style.cursor = 'pointer'; + }); + window.networkCy.on('mouseout', 'node[type="member"]', function() { + document.body.style.cursor = 'default'; + }); + console.log('EQUIPE: Graphe reseau initialise'); } @@ -252,6 +297,7 @@ scatter.push({ value: [j, i], member: m.fullName || m.member, + memberId: m.member, tech: row.technology, availability: m.availability }); @@ -280,6 +326,17 @@ }); window.addEventListener('resize', function() { chart.resize(); }); + + // Ajouter evenement de clic sur les points (membres) + chart.on('click', function(params) { + if (params.data && params.data.memberId) { + var memberId = params.data.memberId; + if (window.__memberProfiles && window.__memberProfiles[memberId]) { + showMemberProfile(memberId, window.__memberProfiles[memberId]); + } + } + }); + console.log('EQUIPE: Matrice de congestion initialisee'); } @@ -301,7 +358,7 @@ if (g.team && g.team.length > 0) { g.team.forEach(function(m) { - h += '
' + + h += '
' + '
' + '
' + (m.fullName || m.member) + '
' + '
' + (m.role || '') + ' - ' + m.seniority + ' - ' + m.coverage + ' technologie(s)
' + @@ -329,9 +386,101 @@ } document.getElementById('genesis-team').innerHTML = h; + + // Ajouter les evenements de clic sur les cartes membres + document.querySelectorAll('.member-card.clickable').forEach(function(card) { + card.addEventListener('click', function() { + var memberId = this.getAttribute('data-member-id'); + if (memberId && window.__memberProfiles && window.__memberProfiles[memberId]) { + showMemberProfile(memberId, window.__memberProfiles[memberId]); + } + }); + }); + console.log('EQUIPE: Equipe de genese initialisee'); } + // Afficher la carte de profil d'un membre + function showMemberProfile(memberId, profile) { + if (!profile) { + console.warn('EQUIPE: Profil non trouve pour', memberId); + return; + } + + // Generer le HTML des competences + var skillsHtml = ''; + if (profile.skillsDetailed && profile.skillsDetailed.length > 0) { + skillsHtml = profile.skillsDetailed.map(function(s) { + var levelClass = s.level || 'beginner'; + var yearsText = s.years ? ' (' + s.years + ' ans)' : ''; + return '' + s.name + yearsText + ''; + }).join(''); + } + + // Generer le HTML des interets + var interestsHtml = ''; + if (profile.interests && profile.interests.length > 0) { + interestsHtml = profile.interests.map(function(i) { + return '' + i + ''; + }).join(''); + } + + // Generer le HTML des projets + var projectsHtml = ''; + if (profile.projects && profile.projects.length > 0) { + projectsHtml = '
    ' + profile.projects.map(function(p) { + return '
  • ' + p + '
  • '; + }).join('') + '
'; + } + + // Generer le HTML des soft skills + var softSkillsHtml = ''; + if (profile.softSkills && profile.softSkills.length > 0) { + softSkillsHtml = profile.softSkills.map(function(s) { + return '' + s + ''; + }).join(''); + } + + var modal = document.createElement('div'); + modal.className = 'profile-modal'; + modal.innerHTML = + '
' + + '' + + '

' + (profile.fullName || memberId) + '

' + + '

' + (profile.role || 'Membre de l\'equipe') + '

' + + '
' + + '' + (profile.availability || 0) + '% disponibilite' + + '' + (profile.yearsExperience || 0) + ' ans exp.' + + '' + (profile.seniorityLevel || 'beginner') + '' + + (profile.joinDate ? 'Depuis ' + profile.joinDate + '' : '') + + '
' + + (skillsHtml ? '

Competences techniques

' + skillsHtml + '
' : '') + + (interestsHtml ? '

Centres d\'interet

' + interestsHtml + '
' : '') + + (softSkillsHtml ? '

Soft Skills

' + softSkillsHtml + '
' : '') + + (projectsHtml ? '

Projets

' + projectsHtml : '') + + (profile.bio ? '

' + profile.bio + '

' : '') + + '
'; + + // Fermer au clic sur le fond ou le bouton + modal.addEventListener('click', function(e) { + if (e.target === modal || e.target.classList.contains('close-btn')) { + modal.remove(); + } + }); + + // Fermer avec Echap + var escHandler = function(e) { + if (e.key === 'Escape') { + modal.remove(); + document.removeEventListener('keydown', escHandler); + } + }; + document.addEventListener('keydown', escHandler); + + document.body.appendChild(modal); + console.log('EQUIPE: Profil affiche pour', memberId); + } + // Demarrer quand le DOM est pret if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', checkAndInitTeamPage); diff --git a/scripts/generate-team-visualization-data.js b/scripts/generate-team-visualization-data.js index 23d2076..347f60c 100755 --- a/scripts/generate-team-visualization-data.js +++ b/scripts/generate-team-visualization-data.js @@ -15,11 +15,12 @@ function extractYaml(yamlContent, key) { return match ? match[1].trim() : null; } -// Extraire les compétences depuis le YAML +// Extraire les compétences depuis le YAML (avec details) function extractSkills(yamlContent) { const skills = []; const lines = yamlContent.split('\n'); let inSkillsSection = false; + let currentSkill = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; @@ -30,6 +31,7 @@ function extractSkills(yamlContent) { } if (inSkillsSection && line.match(/^\w+:/) && !line.match(/^\s+/)) { + if (currentSkill) skills.push(currentSkill); inSkillsSection = false; continue; } @@ -37,12 +39,60 @@ function extractSkills(yamlContent) { if (inSkillsSection) { const nameMatch = line.match(/^\s+-\s+name:\s*["']?([^"'\n]+)["']?/); if (nameMatch) { - skills.push(nameMatch[1]); + if (currentSkill) skills.push(currentSkill); + currentSkill = { name: nameMatch[1], level: 'beginner', years: 0 }; + } + const levelMatch = line.match(/^\s+level:\s*["']?([^"'\n]+)["']?/); + if (levelMatch && currentSkill) currentSkill.level = levelMatch[1]; + const yearsMatch = line.match(/^\s+years:\s*(\d+)/); + if (yearsMatch && currentSkill) currentSkill.years = parseInt(yearsMatch[1]); + } + } + if (currentSkill) skills.push(currentSkill); + + return skills; +} + +// Extraire une liste YAML simple (ex: interests, softSkills, projects) +function extractYamlList(yamlContent, key) { + const items = []; + const lines = yamlContent.split('\n'); + let inSection = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Debut de section + if (line.match(new RegExp(`^${key}:\\s*$`))) { + inSection = true; + continue; + } + + // Fin de section (nouvelle cle au niveau racine) + if (inSection && line.match(/^\w+:/) && !line.match(/^\s+/)) { + inSection = false; + continue; + } + + // Extraction des items + if (inSection) { + const itemMatch = line.match(/^\s+-\s*["']?([^"'\n]+)["']?/); + if (itemMatch) { + items.push(itemMatch[1].trim()); } } } - return skills; + return items; +} + +// Extraire la bio (contenu apres le front matter YAML) +function extractBio(content) { + const parts = content.split(/^---\n[\s\S]*?\n---/m); + if (parts.length > 1) { + return parts[1].trim().replace(/\n/g, ' ').substring(0, 500); + } + return ''; } // Charger les technologies depuis les blips @@ -93,7 +143,7 @@ function loadTechnologies() { return technologies; } -// Charger les membres de l'équipe +// Charger les membres de l'équipe (avec profils complets) function loadTeamMembers() { const teamDir = path.join(__dirname, '../docs/data/team'); const members = []; @@ -112,6 +162,8 @@ function loadTeamMembers() { if (!yamlMatch) continue; const yaml = yamlMatch[1]; + const skillsData = extractSkills(yaml); + const member = { id: extractYaml(yaml, 'name') || file.replace('.md', ''), fullName: extractYaml(yaml, 'fullName') || extractYaml(yaml, 'name'), @@ -119,7 +171,13 @@ function loadTeamMembers() { availability: parseInt(extractYaml(yaml, 'availability') || '0'), seniorityLevel: extractYaml(yaml, 'seniorityLevel') || 'beginner', yearsExperience: parseInt(extractYaml(yaml, 'yearsExperience') || '0'), - skills: extractSkills(yaml) + joinDate: extractYaml(yaml, 'joinDate') || '', + skills: skillsData.map(s => typeof s === 'string' ? s : s.name), + skillsDetailed: skillsData, + interests: extractYamlList(yaml, 'interests'), + softSkills: extractYamlList(yaml, 'softSkills'), + projects: extractYamlList(yaml, 'projects'), + bio: extractBio(content) }; members.push(member); @@ -342,10 +400,30 @@ function main() { console.log(`✅ ${technologies.length} technologies chargées`); console.log(`✅ ${members.length} membres chargés`); + // Creer un index des profils membres pour acces rapide + const memberProfiles = {}; + members.forEach(m => { + memberProfiles[m.id] = { + id: m.id, + fullName: m.fullName, + role: m.role, + availability: m.availability, + seniorityLevel: m.seniorityLevel, + yearsExperience: m.yearsExperience, + joinDate: m.joinDate, + skillsDetailed: m.skillsDetailed, + interests: m.interests, + softSkills: m.softSkills, + projects: m.projects, + bio: m.bio + }; + }); + const data = { network: generateNetworkData(technologies, members), congestionMatrix: generateCongestionMatrix(technologies, members), genesisTeam: generateGenesisTeam(technologies, members), + memberProfiles: memberProfiles, technologies: technologies, members: members, generatedAt: new Date().toISOString()