diff --git a/Dockerfile.business b/Dockerfile.business
index 925d545..31820ec 100644
--- a/Dockerfile.business
+++ b/Dockerfile.business
@@ -100,480 +100,8 @@ RUN echo "đ Comptage des fichiers .md dans .techradar/data/radar" && \
# Créer la page Next.js /team ET un fichier HTML statique /team/index.html
# 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, 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 [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(() => {
- // Charger les données équipe
- loadTeamData();
- }, []);
-
- 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
Données équipe non disponibles
;
-
- const genesis = data.genesisTeam;
- return (
-
-
Ăquipe de GenĂšse MVP
-
-
CritÚres de Sélection
-
- - Disponibilité ℠{genesis.minAvailability}%
- - Couverture technologique â„ {genesis.minCoverage} technologies
- - Taille équipe optimale: {genesis.targetTeamSize} membres
-
-
-
-
- {genesis.selectedMembers.map(member => (
-
-
-
-
{member.fullName || member.member}
-
- {member.role || ''} âą {member.seniority} âą {member.coverage} technologie(s)
-
-
-
-
{member.availability}%
-
Disponibilité
-
-
-
- {member.skills.slice(0, 8).map(skill => (
-
- {skill}
-
- ))}
- {member.skills.length > 8 && (
-
- +{member.skills.length - 8}
-
- )}
-
-
- ))}
-
-
- );
- };
-
- if (loading) {
- return (
-
-
- Chargement des visualisations équipe...
-
-
- );
- }
-
- if (error) {
- return (
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
- {activeTab === 'network' && (
-
-
Graphe Réseau - Technologies et Compétences
-
-
-
-
-
Technologies Avancées
-
-
-
-
Technologies Utilitaires
-
-
-
-
-
- )}
-
- {activeTab === 'congestion' && (
-
-
Matrice de Congestion - Technologies Core
-
-
- )}
-
- {activeTab === 'genesis' && (
-
- {renderGenesisTeam()}
-
- )}
-
-
- );
-}
-EOF
+RUN mkdir -p .techradar/src/pages
+COPY docker/team-page.tsx .techradar/src/pages/team.tsx
# Script Python pour ajouter le lien Ăquipe dans Navigation.tsx (supprime TOUS les doublons)
RUN cat > /tmp/add_team_link.py << 'PYEOF'
diff --git a/docker/team-page.tsx b/docker/team-page.tsx
new file mode 100644
index 0000000..b5f8069
--- /dev/null
+++ b/docker/team-page.tsx
@@ -0,0 +1,578 @@
+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 [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(() => {
+ // Charger les données équipe
+ loadTeamData();
+ }, []);
+
+ 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 Données équipe non disponibles
;
+
+ const genesis = data.genesisTeam;
+ return (
+
+
Ăquipe de GenĂšse MVP
+
+
CritÚres de Sélection
+
+ - Disponibilité ℠{genesis.minAvailability}%
+ - Couverture technologique â„ {genesis.minCoverage} technologies
+ - Taille équipe optimale: {genesis.targetTeamSize} membres
+
+
+
+
+ {genesis.selectedMembers.map((member) => (
+
+
+
+
+ {member.fullName || member.member}
+
+
+ {member.role || ''} âą {member.seniority} âą {member.coverage} technologie(s)
+
+
+
+
+ {member.availability}%
+
+
Disponibilité
+
+
+
+ {member.skills.slice(0, 8).map((skill) => (
+
+ {skill}
+
+ ))}
+ {member.skills.length > 8 && (
+
+ +{member.skills.length - 8}
+
+ )}
+
+
+ ))}
+
+
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+ Chargement des visualisations équipe...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {activeTab === 'network' && (
+
+
Graphe Réseau - Technologies et Compétences
+
+
+
+
+
Technologies Avancées
+
+
+
+
Technologies Utilitaires
+
+
+
+
+
+ )}
+
+ {activeTab === 'congestion' && (
+
+
Matrice de Congestion - Technologies Core
+
+
+ )}
+
+ {activeTab === 'genesis' && (
+
+ {renderGenesisTeam()}
+
+ )}
+
+
+ );
+}