diff --git a/Dockerfile.business b/Dockerfile.business
index 0ffe434..72b5fdc 100644
--- a/Dockerfile.business
+++ b/Dockerfile.business
@@ -32,7 +32,7 @@ RUN apk add --no-cache git python3
COPY package.json package-lock.json* ./
# Installation des dépendances Node
-RUN npm install --legacy-peer-deps --ignore-scripts
+RUN npm install --legacy-peer-deps --ignore-scripts cytoscape cytoscape-cose-bilkent echarts-for-react
# Patch du package aoe_technology_radar pour inclure gray-matter dans les dépendances runtime
RUN node -e "const fs=require('fs');const pkgPath='./node_modules/aoe_technology_radar/package.json';const pkg=JSON.parse(fs.readFileSync(pkgPath,'utf8'));pkg.dependencies=pkg.dependencies||{};pkg.dependencies['gray-matter']='^4.0.3';pkg.dependencies['postcss']='^8.4.47';pkg.scripts=pkg.scripts||{};pkg.scripts.prepare='';fs.writeFileSync(pkgPath,JSON.stringify(pkg,null,2));"
@@ -50,7 +50,7 @@ RUN mkdir -p .techradar && \
RUN node -e "const crypto=require('crypto');const fs=require('fs');const hash=crypto.createHash('sha256').update(fs.readFileSync('package.json')).digest('hex');fs.writeFileSync('.techradar/hash',hash);"
RUN node -e "const fs=require('fs');const p='.techradar/package.json';if(!fs.existsSync(p)){console.error('.techradar/package.json not found');process.exit(1);}const pkg=JSON.parse(fs.readFileSync(p,'utf8'));pkg.scripts=pkg.scripts||{};pkg.scripts.prepare='';fs.writeFileSync(p,JSON.stringify(pkg,null,2));"
# Installer les dépendances dans .techradar (y compris devDependencies pour tsx nécessaire à build:data)
-RUN cd .techradar && npm install --legacy-peer-deps --include=dev
+RUN cd .techradar && npm install --legacy-peer-deps --include=dev cytoscape cytoscape-cose-bilkent echarts-for-react
RUN cd .techradar && npm run build:icons
# --- CONFIGURATION BUSINESS ---
@@ -101,18 +101,478 @@ RUN echo "📊 Comptage des fichiers .md dans .techradar/data/radar" && \
# La page Next.js pour le routing, le HTML statique pour garantir l'affichage
RUN mkdir -p .techradar/src/pages && \
cat > .techradar/src/pages/team.tsx << 'EOF'
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef } from 'react';
+import dynamic from 'next/dynamic';
+
+// Chargement dynamique des bibliothèques pour éviter les erreurs SSR
+const CytoscapeComponent = dynamic(() => import('cytoscape'), { ssr: false });
+const EChartsComponent = dynamic(() => import('echarts-for-react'), { ssr: false });
export default function TeamPage() {
- const [htmlContent, setHtmlContent] = useState('');
+ const [activeTab, setActiveTab] = useState('network');
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const networkRef = useRef(null);
+ const matrixRef = useRef(null);
useEffect(() => {
- // Intégrer directement le HTML statique dans la page React
- // Cela évite les problèmes de routing Next.js
- console.log('🔄 TEAM PAGE: Chargement direct du contenu HTML intégré');
+ // Charger les données équipe
+ loadTeamData();
+ }, []);
- // Le contenu HTML est intégré directement dans le composant
- const staticHtml = `
+ useEffect(() => {
+ // Initialiser les graphiques quand les données sont chargées et l'onglet actif change
+ if (data && !loading) {
+ if (activeTab === 'network') {
+ initNetworkGraph();
+ } else if (activeTab === 'congestion') {
+ initCongestionMatrix();
+ }
+ }
+ }, [data, loading, activeTab]);
+
+ const loadTeamData = async () => {
+ try {
+ console.log('🔄 Chargement des données équipe...');
+ const response = await fetch('/team-visualization-data.json');
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+ const jsonData = await response.json();
+ console.log('✅ Données chargées:', Object.keys(jsonData));
+ setData(jsonData);
+ } catch (err) {
+ console.error('❌ Erreur chargement données:', err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const initNetworkGraph = () => {
+ if (!data?.network || !networkRef.current) return;
+
+ console.log('📊 Initialisation graphe réseau...');
+
+ // Import dynamique pour éviter les erreurs SSR
+ import('cytoscape').then((cytoscape) => {
+ import('cytoscape-cose-bilkent').then(() => {
+ const cy = cytoscape.default({
+ container: networkRef.current,
+ elements: data.network,
+ style: [
+ {
+ selector: 'node[type="technology"]',
+ style: {
+ 'background-color': 'data(color)',
+ 'label': 'data(label)',
+ 'width': function(ele) {
+ const coverage = ele.data('coverage') || 0;
+ return Math.max(30, 30 + (coverage * 8));
+ },
+ 'height': function(ele) {
+ const coverage = ele.data('coverage') || 0;
+ return Math.max(30, 30 + (coverage * 8));
+ },
+ 'font-size': '12px',
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ 'color': '#ffffff',
+ 'text-outline-color': '#1a4d3a',
+ 'text-outline-width': '2px'
+ }
+ },
+ {
+ selector: 'node[type="member"]',
+ style: {
+ 'background-color': '#88ff88',
+ 'label': 'data(label)',
+ 'width': function(ele) {
+ const availability = ele.data('availability') || 0;
+ return Math.max(25, 25 + (availability / 3));
+ },
+ 'height': function(ele) {
+ const availability = ele.data('availability') || 0;
+ return Math.max(25, 25 + (availability / 3));
+ },
+ 'font-size': '10px',
+ 'text-valign': 'center',
+ 'text-halign': 'center',
+ 'color': '#1a4d3a',
+ 'text-outline-color': '#ffffff',
+ 'text-outline-width': '1px'
+ }
+ },
+ {
+ selector: 'edge',
+ style: {
+ 'width': function(ele) {
+ return 1 + (ele.data('weight') || 0.5);
+ },
+ 'line-color': '#4ade80',
+ 'target-arrow-color': '#4ade80',
+ 'target-arrow-shape': 'triangle',
+ 'curve-style': 'bezier'
+ }
+ }
+ ],
+ layout: {
+ name: 'cose-bilkent',
+ animate: true,
+ animationDuration: 1000,
+ nodeRepulsion: 4500,
+ idealEdgeLength: 100,
+ edgeElasticity: 0.45
+ }
+ });
+
+ // Gestionnaire de clics
+ cy.on('tap', 'node', function(evt) {
+ const node = evt.target;
+ const nodeData = node.data();
+ let tooltip = '';
+
+ if (nodeData.type === 'technology') {
+ tooltip = \`\${nodeData.label}\\n\` +
+ \`Ring: \${nodeData.ring}\\n\` +
+ \`Couverture: \${nodeData.coverage} personne(s)\\n\` +
+ \`Impact: \${nodeData.businessImpact}\\n\` +
+ \`Gap: \${nodeData.skillGap}\`;
+ } else {
+ tooltip = \`\${nodeData.label}\\n\` +
+ \`Disponibilité: \${nodeData.availability}%\` +
+ (nodeData.role ? \`\\nRôle: \${nodeData.role}\` : '');
+ }
+
+ alert(tooltip);
+ });
+
+ console.log('✅ Graphe réseau initialisé');
+ });
+ });
+ };
+
+ const initCongestionMatrix = () => {
+ if (!data?.congestionMatrix || !matrixRef.current) return;
+
+ console.log('📈 Initialisation matrice congestion...');
+
+ // Import dynamique pour éviter les erreurs SSR
+ import('echarts-for-react').then(() => {
+ import('echarts').then((echarts) => {
+ const techs = data.congestionMatrix.map(r => r.technology);
+ const members = data.congestionMatrix[0]?.members.map(m => m.fullName || m.member) || [];
+
+ const scatterData = [];
+ data.congestionMatrix.forEach((row, i) => {
+ row.members.forEach((member, j) => {
+ scatterData.push([j, i, member.availability, member]);
+ });
+ });
+
+ const chart = echarts.default.init(matrixRef.current);
+ const option = {
+ title: {
+ text: 'Disponibilité des Technologies Core',
+ left: 'center',
+ textStyle: { color: '#e0e0e0' }
+ },
+ tooltip: {
+ formatter: function(params) {
+ if (params.data && params.data[3]) {
+ const member = params.data[3];
+ return \`\${member.fullName || member.member}
\` +
+ \`Technologie: \${techs[params.data[1]]}
\` +
+ \`Disponibilité: \${params.data[2]}%\`;
+ }
+ }
+ },
+ grid: {
+ left: '10%',
+ right: '10%',
+ top: '15%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ data: members,
+ axisLabel: {
+ color: '#e0e0e0',
+ rotate: 45
+ }
+ },
+ yAxis: {
+ type: 'category',
+ data: techs,
+ axisLabel: { color: '#e0e0e0' }
+ },
+ visualMap: {
+ min: 0,
+ max: 100,
+ dimension: 2,
+ orient: 'horizontal',
+ left: 'center',
+ top: 'top',
+ text: ['Élevé', 'Faible'],
+ textStyle: { color: '#e0e0e0' },
+ inRange: {
+ color: ['#ff4444', '#ff8800', '#4488ff', '#88ff88']
+ },
+ outOfRange: {
+ color: '#444444'
+ }
+ },
+ series: [{
+ name: 'Disponibilité',
+ type: 'scatter',
+ data: scatterData,
+ symbolSize: function(data) {
+ return 15 + (data[2] || 0) / 2;
+ }
+ }]
+ };
+
+ chart.setOption(option);
+ console.log('✅ Matrice congestion initialisée');
+ });
+ });
+ };
+
+ const renderGenesisTeam = () => {
+ if (!data?.genesisTeam) return
Visualisation des compétences et identification de l'équipe de genèse MVP
+