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:
@@ -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'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'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'é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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user