Files
towercontrol/src/app/jira-dashboard/JiraDashboardPageClient.tsx
Julien Froidefond a0e2a78372 feat: update Daily and Jira dashboard pages with dynamic titles and improved UI
- Implemented `getTodayTitle` and `getYesterdayTitle` functions in `DailyPageClient` to dynamically set section titles based on the current date.
- Updated `TODO.md` to mark completed tasks related to the Jira dashboard UI consistency.
- Enhanced card content in `JiraDashboardPageClient` to ensure charts are responsive and maintain consistent styling.
- Removed unused date formatting function in `DailySection` for cleaner code.
2025-09-21 10:49:39 +02:00

766 lines
31 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect, useMemo } from 'react';
import { JiraConfig } from '@/lib/types';
import { useJiraAnalytics } from '@/hooks/useJiraAnalytics';
import { useJiraExport } from '@/hooks/useJiraExport';
import { filterAnalyticsByPeriod, getPeriodInfo, type PeriodFilter } from '@/lib/jira-period-filter';
import { Header } from '@/components/ui/Header';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { VelocityChart } from '@/components/jira/VelocityChart';
import { TeamDistributionChart } from '@/components/jira/TeamDistributionChart';
import { CycleTimeChart } from '@/components/jira/CycleTimeChart';
import { TeamActivityHeatmap } from '@/components/jira/TeamActivityHeatmap';
import { BurndownChart } from '@/components/jira/BurndownChart';
import { ThroughputChart } from '@/components/jira/ThroughputChart';
import { QualityMetrics } from '@/components/jira/QualityMetrics';
import { PredictabilityMetrics } from '@/components/jira/PredictabilityMetrics';
import { CollaborationMatrix } from '@/components/jira/CollaborationMatrix';
import { SprintComparison } from '@/components/jira/SprintComparison';
import AnomalyDetectionPanel from '@/components/jira/AnomalyDetectionPanel';
import FilterBar from '@/components/jira/FilterBar';
import SprintDetailModal, { SprintDetails } from '@/components/jira/SprintDetailModal';
import { getSprintDetails } from '../../actions/jira-sprint-details';
import { useJiraFilters } from '@/hooks/useJiraFilters';
import { SprintVelocity } from '@/lib/types';
import Link from 'next/link';
interface JiraDashboardPageClientProps {
initialJiraConfig: JiraConfig;
}
export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPageClientProps) {
const { analytics: rawAnalytics, isLoading, error, loadAnalytics, refreshAnalytics } = useJiraAnalytics();
const { isExporting, error: exportError, exportCSV, exportJSON } = useJiraExport();
const {
availableFilters,
activeFilters,
filteredAnalytics,
applyFilters,
hasActiveFilters
} = useJiraFilters();
const [selectedPeriod, setSelectedPeriod] = useState<PeriodFilter>('current');
const [selectedSprint, setSelectedSprint] = useState<SprintVelocity | null>(null);
const [showSprintModal, setShowSprintModal] = useState(false);
const [activeTab, setActiveTab] = useState<'overview' | 'velocity' | 'analytics' | 'quality'>('overview');
// Filtrer les analytics selon la période sélectionnée et les filtres avancés
const analytics = useMemo(() => {
const baseAnalytics = hasActiveFilters && filteredAnalytics ? filteredAnalytics : rawAnalytics;
if (!baseAnalytics) return null;
return filterAnalyticsByPeriod(baseAnalytics, selectedPeriod);
}, [rawAnalytics, filteredAnalytics, selectedPeriod, hasActiveFilters]);
// Informations sur la période pour l'affichage
const periodInfo = getPeriodInfo(selectedPeriod);
useEffect(() => {
// Charger les analytics au montage si Jira est configuré avec un projet
if (initialJiraConfig.enabled && initialJiraConfig.projectKey) {
loadAnalytics();
}
}, [initialJiraConfig.enabled, initialJiraConfig.projectKey, loadAnalytics]);
// Gestion du clic sur un sprint
const handleSprintClick = (sprint: SprintVelocity) => {
setSelectedSprint(sprint);
setShowSprintModal(true);
};
const handleCloseSprintModal = () => {
setShowSprintModal(false);
setSelectedSprint(null);
};
const loadSprintDetails = async (sprintName: string): Promise<SprintDetails> => {
const result = await getSprintDetails(sprintName);
if (result.success && result.data) {
return result.data;
} else {
throw new Error(result.error || 'Erreur lors du chargement des détails du sprint');
}
};
// Vérifier si Jira est configuré
const isJiraConfigured = initialJiraConfig.enabled &&
initialJiraConfig.baseUrl &&
initialJiraConfig.email &&
initialJiraConfig.apiToken;
const hasProjectConfigured = isJiraConfigured && initialJiraConfig.projectKey;
if (!isJiraConfigured) {
return (
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Dashboard Jira - Analytics d'équipe"
/>
<div className="container mx-auto px-4 py-8">
<Card className="max-w-2xl mx-auto">
<CardHeader>
<h2 className="text-xl font-semibold"> Configuration requise</h2>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-[var(--muted-foreground)]">
Jira n&apos;est pas configuré. Vous devez d&apos;abord configurer votre connexion Jira
pour accéder aux analytics d&apos;équipe.
</p>
<Link href="/settings/integrations">
<Button variant="primary">
Configurer Jira
</Button>
</Link>
</CardContent>
</Card>
</div>
</div>
);
}
if (!hasProjectConfigured) {
return (
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Dashboard Jira - Analytics d'équipe"
/>
<div className="container mx-auto px-4 py-8">
<Card className="max-w-2xl mx-auto">
<CardHeader>
<h2 className="text-xl font-semibold">🎯 Projet requis</h2>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-[var(--muted-foreground)]">
Aucun projet n&apos;est configuré pour les analytics d&apos;équipe.
Configurez un projet spécifique à surveiller dans les paramètres Jira.
</p>
<Link href="/settings/integrations">
<Button variant="primary">
Configurer un projet
</Button>
</Link>
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle={`Analytics Jira - Projet ${initialJiraConfig.projectKey}`}
/>
<div className="container mx-auto px-4 py-4">
<div className="max-w-7xl mx-auto">
{/* Breadcrumb */}
<div className="mb-4 text-sm">
<Link href="/settings" className="text-[var(--muted-foreground)] hover:text-[var(--primary)]">
Paramètres
</Link>
<span className="mx-2 text-[var(--muted-foreground)]">/</span>
<Link href="/settings/integrations" className="text-[var(--muted-foreground)] hover:text-[var(--primary)]">
Intégrations
</Link>
<span className="mx-2 text-[var(--muted-foreground)]">/</span>
<span className="text-[var(--foreground)]">Dashboard Jira</span>
</div>
{/* Header avec contrôles */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] mb-2">
📊 Analytics d&apos;équipe
</h1>
<div className="space-y-1">
<p className="text-[var(--muted-foreground)]">
Surveillance en temps réel du projet {initialJiraConfig.projectKey}
</p>
<p className="text-sm text-[var(--primary)] flex items-center gap-1">
<span>{periodInfo.icon}</span>
<span>{periodInfo.label}</span>
<span className="text-[var(--muted-foreground)]"> {periodInfo.description}</span>
</p>
</div>
</div>
<div className="flex items-center gap-3">
{/* Sélecteur de période */}
<div className="flex bg-[var(--card)] border border-[var(--border)] rounded-lg p-1">
{[
{ value: '7d', label: '7j' },
{ value: '30d', label: '30j' },
{ value: '3m', label: '3m' },
{ value: 'current', label: 'Sprint' }
].map((period: { value: string; label: string }) => (
<button
key={period.value}
onClick={() => setSelectedPeriod(period.value as PeriodFilter)}
className={`px-3 py-1 text-sm rounded transition-all ${
selectedPeriod === period.value
? 'bg-[var(--primary)] text-[var(--primary-foreground)]'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
{period.label}
</button>
))}
</div>
<div className="flex items-center gap-2">
{analytics && (
<>
<div className="text-xs text-[var(--muted-foreground)] px-2 py-1 bg-[var(--card)] border border-[var(--border)] rounded">
💾 Données en cache
</div>
{/* Boutons d'export */}
<div className="flex items-center gap-1">
<Button
onClick={exportCSV}
disabled={isExporting}
variant="ghost"
className="text-xs px-2 py-1 h-auto"
>
{isExporting ? '⏳' : '📊'} CSV
</Button>
<Button
onClick={exportJSON}
disabled={isExporting}
variant="ghost"
className="text-xs px-2 py-1 h-auto"
>
{isExporting ? '⏳' : '📄'} JSON
</Button>
</div>
</>
)}
<Button
onClick={refreshAnalytics}
disabled={isLoading}
variant="secondary"
>
{isLoading ? '🔄 Actualisation...' : '🔄 Actualiser'}
</Button>
</div>
</div>
</div>
{/* Contenu principal */}
{error && (
<Card className="mb-6 border-red-500/20 bg-red-500/10">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
<span></span>
<span>{error}</span>
</div>
</CardContent>
</Card>
)}
{exportError && (
<Card className="mb-6 border-orange-500/20 bg-orange-500/10">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400">
<span></span>
<span>Erreur d&apos;export: {exportError}</span>
</div>
</CardContent>
</Card>
)}
{isLoading && !analytics && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Skeleton loading */}
{[1, 2, 3, 4, 5, 6].map(i => (
<Card key={i} className="animate-pulse">
<CardContent className="p-6">
<div className="h-4 bg-[var(--muted)] rounded mb-4"></div>
<div className="h-8 bg-[var(--muted)] rounded mb-2"></div>
<div className="h-4 bg-[var(--muted)] rounded w-2/3"></div>
</CardContent>
</Card>
))}
</div>
)}
{analytics && (
<div className="space-y-6">
{/* En-tête compact du projet */}
<Card>
<CardHeader className="pb-4">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
🎯 {analytics.project.name}
<span className="text-sm font-normal text-[var(--muted-foreground)]">
({periodInfo.label})
</span>
</h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-xl font-bold text-[var(--primary)]">
{analytics.project.totalIssues}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Tickets
</div>
</div>
<div className="text-center">
<div className="text-xl font-bold text-blue-500">
{analytics.teamMetrics.totalAssignees}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Équipe
</div>
</div>
<div className="text-center">
<div className="text-xl font-bold text-green-500">
{analytics.teamMetrics.activeAssignees}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Actifs
</div>
</div>
<div className="text-center">
<div className="text-xl font-bold text-orange-500">
{analytics.velocityMetrics.currentSprintPoints}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Points
</div>
</div>
</div>
</div>
</CardHeader>
</Card>
{/* Barre de filtres */}
<FilterBar
availableFilters={availableFilters}
activeFilters={activeFilters}
onFiltersChange={applyFilters}
/>
{/* Détection d'anomalies */}
<AnomalyDetectionPanel />
{/* Onglets de navigation */}
<div className="border-b border-[var(--border)]">
<nav className="flex space-x-8">
{[
{ id: 'overview', label: '📊 Vue d\'ensemble' },
{ id: 'velocity', label: '🚀 Vélocité & Sprints' },
{ id: 'analytics', label: '📈 Analytics avancées' },
{ id: 'quality', label: '🎯 Qualité & Collaboration' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as 'overview' | 'velocity' | 'analytics' | 'quality')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.id
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:border-[var(--border)]'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Contenu des onglets */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Graphiques principaux */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold">👥 Répartition de l&apos;équipe</h3>
</CardHeader>
<CardContent>
<TeamDistributionChart
distribution={analytics.teamMetrics.issuesDistribution}
className="h-64"
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">🚀 Vélocité des sprints</h3>
</CardHeader>
<CardContent>
<VelocityChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-64"
onSprintClick={handleSprintClick}
/>
</CardContent>
</Card>
</div>
{/* Métriques de temps et cycle time */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold"> Cycle Time par type</h3>
</CardHeader>
<CardContent>
<CycleTimeChart
cycleTimeByType={analytics.cycleTimeMetrics.cycleTimeByType}
className="h-64"
/>
<div className="mt-4 text-center">
<div className="text-2xl font-bold text-[var(--primary)]">
{analytics.cycleTimeMetrics.averageCycleTime}
</div>
<div className="text-sm text-[var(--muted-foreground)]">
jours en moyenne globale
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">🚀 Vélocité</h3>
</CardHeader>
<CardContent>
<div className="mb-4">
<div className="text-3xl font-bold text-green-500">
{analytics.velocityMetrics.averageVelocity}
</div>
<div className="text-sm text-[var(--muted-foreground)]">
points par sprint
</div>
</div>
<div className="space-y-2">
{analytics.velocityMetrics.sprintHistory.map(sprint => (
<div key={sprint.sprintName} className="text-sm">
<div className="flex justify-between">
<span>{sprint.sprintName}</span>
<span className="font-mono">
{sprint.completedPoints}/{sprint.plannedPoints}
</span>
</div>
<div className="w-full bg-[var(--muted)] rounded-full h-1.5 mt-1">
<div
className="bg-green-500 h-1.5 rounded-full"
style={{ width: `${sprint.completionRate}%` }}
></div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Métriques avancées */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold">📉 Burndown Chart</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-96 overflow-hidden">
<BurndownChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">📈 Throughput</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-96 overflow-hidden">
<ThroughputChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
</div>
{/* Métriques de qualité */}
<Card>
<CardHeader>
<h3 className="font-semibold">🎯 Métriques de qualité</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full overflow-hidden">
<QualityMetrics
analytics={analytics}
className="min-h-96 w-full"
/>
</div>
</CardContent>
</Card>
{/* Métriques de predictabilité */}
<Card>
<CardHeader>
<h3 className="font-semibold">📊 Predictabilité</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full overflow-hidden">
<PredictabilityMetrics
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-auto w-full"
/>
</div>
</CardContent>
</Card>
{/* Matrice de collaboration - ligne entière */}
<Card>
<CardHeader>
<h3 className="font-semibold">🤝 Matrice de collaboration</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full overflow-hidden">
<CollaborationMatrix
analytics={analytics}
className="h-auto w-full"
/>
</div>
</CardContent>
</Card>
{/* Comparaison inter-sprints */}
<Card>
<CardHeader>
<h3 className="font-semibold">📊 Comparaison inter-sprints</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full overflow-hidden">
<SprintComparison
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-auto w-full"
/>
</div>
</CardContent>
</Card>
{/* Heatmap d'activité de l'équipe */}
<Card>
<CardHeader>
<h3 className="font-semibold">🔥 Heatmap d&apos;activité de l&apos;équipe</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full overflow-hidden">
<TeamActivityHeatmap
workloadByAssignee={analytics.workInProgress.byAssignee}
statusDistribution={analytics.workInProgress.byStatus}
className="min-h-96 w-full"
/>
</div>
</CardContent>
</Card>
</div>
)}
{activeTab === 'velocity' && (
<div className="space-y-6">
{/* Graphique de vélocité */}
<Card>
<CardHeader>
<h3 className="font-semibold">🚀 Vélocité des sprints</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<VelocityChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-full w-full"
onSprintClick={handleSprintClick}
/>
</div>
</CardContent>
</Card>
{/* Burndown et Throughput */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold">📉 Burndown Chart</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-96 overflow-hidden">
<BurndownChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">📊 Throughput</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-96 overflow-hidden">
<ThroughputChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
</div>
{/* Comparaison des sprints */}
<Card>
<CardHeader>
<h3 className="font-semibold">📊 Comparaison des sprints</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full overflow-hidden">
<SprintComparison
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-auto w-full"
/>
</div>
</CardContent>
</Card>
</div>
)}
{activeTab === 'analytics' && (
<div className="space-y-6">
{/* Métriques de temps et cycle time */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold"> Cycle Time par type</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<CycleTimeChart
cycleTimeByType={analytics.cycleTimeMetrics.cycleTimeByType}
className="h-full w-full"
/>
</div>
<div className="mt-4 text-center">
<div className="text-2xl font-bold text-[var(--primary)]">
{analytics.cycleTimeMetrics.averageCycleTime.toFixed(1)}
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Cycle time moyen (jours)
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">🔥 Heatmap d&apos;activité</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<TeamActivityHeatmap
workloadByAssignee={analytics.workInProgress.byAssignee}
statusDistribution={analytics.workInProgress.byStatus}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
</div>
{/* Métriques avancées */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold">🎯 Métriques de qualité</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<QualityMetrics
analytics={analytics}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">📈 Predictabilité</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<PredictabilityMetrics
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
</div>
</div>
)}
{activeTab === 'quality' && (
<div className="space-y-6">
{/* Collaboration et équipe */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold">👥 Répartition de l&apos;équipe</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<TeamDistributionChart
distribution={analytics.teamMetrics.issuesDistribution}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">🤝 Matrice de collaboration</h3>
</CardHeader>
<CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<CollaborationMatrix
analytics={analytics}
className="h-full w-full"
/>
</div>
</CardContent>
</Card>
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Modal de détail de sprint */}
<SprintDetailModal
isOpen={showSprintModal}
onClose={handleCloseSprintModal}
sprint={selectedSprint}
onLoadSprintDetails={loadSprintDetails}
/>
</div>
);
}