feat: enhance Jira dashboard with advanced filtering and sprint details

- Updated `TODO.md` to mark several tasks as complete, including anomaly detection and sprint detail integration.
- Enhanced `VelocityChart` to support click events for sprint details, improving user interaction.
- Added `FilterBar` and `AnomalyDetectionPanel` components to `JiraDashboardPageClient` for advanced filtering capabilities.
- Implemented state management for selected sprints and modal display for detailed sprint information.
- Introduced new types for advanced filtering in `types.ts`, expanding the filtering options available in the analytics.
This commit is contained in:
Julien Froidefond
2025-09-19 10:13:48 +02:00
parent b7707d7651
commit 3dd6e0fd1c
17 changed files with 2879 additions and 68 deletions

View File

@@ -18,6 +18,12 @@ 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 {
@@ -27,13 +33,24 @@ interface JiraDashboardPageClientProps {
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
// Filtrer les analytics selon la période sélectionnée et les filtres avancés
const analytics = useMemo(() => {
if (!rawAnalytics) return null;
return filterAnalyticsByPeriod(rawAnalytics, selectedPeriod);
}, [rawAnalytics, selectedPeriod]);
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);
@@ -45,6 +62,26 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
}
}, [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 &&
@@ -256,51 +293,92 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
{analytics && (
<div className="space-y-6">
{/* Vue d'ensemble du projet */}
{/* En-tête compact du projet */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold flex items-center gap-2">
🎯 Vue d&apos;ensemble - {analytics.project.name}
</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-[var(--primary)]">
{analytics.project.totalIssues}
<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-sm text-[var(--muted-foreground)]">
Total tickets
<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>
<div className="text-center">
<div className="text-2xl font-bold text-blue-500">
{analytics.teamMetrics.totalAssignees}
<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-sm text-[var(--muted-foreground)]">
Membres équipe
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-500">
{analytics.teamMetrics.activeAssignees}
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Actifs
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-orange-500">
{analytics.velocityMetrics.currentSprintPoints}
</div>
<div className="text-sm text-[var(--muted-foreground)]">
Points complétés
<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>
</CardContent>
</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>
@@ -323,6 +401,7 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<VelocityChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-64"
onSprintClick={handleSprintClick}
/>
</CardContent>
</Card>
@@ -477,10 +556,176 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
/>
</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>
<VelocityChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-64"
onSprintClick={handleSprintClick}
/>
</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>
<BurndownChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-96"
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">📊 Throughput</h3>
</CardHeader>
<CardContent>
<ThroughputChart
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-96"
/>
</CardContent>
</Card>
</div>
{/* Comparaison des sprints */}
<Card>
<CardHeader>
<h3 className="font-semibold">📊 Comparaison des sprints</h3>
</CardHeader>
<CardContent>
<SprintComparison
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-auto"
/>
</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>
<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.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>
<TeamActivityHeatmap
workloadByAssignee={analytics.workInProgress.byAssignee}
statusDistribution={analytics.workInProgress.byStatus}
className="h-64"
/>
</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>
<QualityMetrics
analytics={analytics}
className="h-64"
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">📈 Predictabilité</h3>
</CardHeader>
<CardContent>
<PredictabilityMetrics
sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-64"
/>
</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>
<TeamDistributionChart
distribution={analytics.teamMetrics.issuesDistribution}
className="h-64"
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">🤝 Matrice de collaboration</h3>
</CardHeader>
<CardContent>
<CollaborationMatrix
analytics={analytics}
className="h-64"
/>
</CardContent>
</Card>
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Modal de détail de sprint */}
<SprintDetailModal
isOpen={showSprintModal}
onClose={handleCloseSprintModal}
sprint={selectedSprint}
onLoadSprintDetails={loadSprintDetails}
/>
</div>
);
}