'use client'; import { useState, useEffect, useCallback } from 'react'; import { SprintVelocity, JiraTask, AssigneeDistribution, StatusDistribution } from '@/lib/types'; import { Modal } from '@/components/ui/Modal'; import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { Button } from '@/components/ui/Button'; interface SprintDetailModalProps { isOpen: boolean; onClose: () => void; sprint: SprintVelocity | null; onLoadSprintDetails: (sprintName: string) => Promise; } export interface SprintDetails { sprint: SprintVelocity; issues: JiraTask[]; assigneeDistribution: AssigneeDistribution[]; statusDistribution: StatusDistribution[]; metrics: { totalIssues: number; completedIssues: number; inProgressIssues: number; blockedIssues: number; averageCycleTime: number; velocityTrend: 'up' | 'down' | 'stable'; }; } export default function SprintDetailModal({ isOpen, onClose, sprint, onLoadSprintDetails }: SprintDetailModalProps) { const [sprintDetails, setSprintDetails] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedTab, setSelectedTab] = useState<'overview' | 'issues' | 'metrics'>('overview'); const [selectedAssignee, setSelectedAssignee] = useState(null); const [selectedStatus, setSelectedStatus] = useState(null); const loadSprintDetails = useCallback(async () => { if (!sprint) return; setLoading(true); setError(null); try { const details = await onLoadSprintDetails(sprint.sprintName); setSprintDetails(details); } catch (err) { setError(err instanceof Error ? err.message : 'Erreur lors du chargement'); } finally { setLoading(false); } }, [sprint, onLoadSprintDetails]); // Charger les détails du sprint quand le modal s'ouvre useEffect(() => { if (isOpen && sprint && !sprintDetails) { loadSprintDetails(); } }, [isOpen, sprint, sprintDetails, loadSprintDetails]); // Reset quand on change de sprint useEffect(() => { if (sprint) { setSprintDetails(null); setSelectedAssignee(null); setSelectedStatus(null); setSelectedTab('overview'); } }, [sprint]); // Filtrer les issues selon les sélections const filteredIssues = sprintDetails?.issues.filter(issue => { if (selectedAssignee && (issue.assignee?.displayName || 'Non assigné') !== selectedAssignee) { return false; } if (selectedStatus && issue.status.name !== selectedStatus) { return false; } return true; }) || []; const getStatusColor = (status: string): string => { if (status.toLowerCase().includes('done') || status.toLowerCase().includes('closed')) { return 'bg-green-100 text-green-800'; } if (status.toLowerCase().includes('progress') || status.toLowerCase().includes('review')) { return 'bg-blue-100 text-blue-800'; } if (status.toLowerCase().includes('blocked') || status.toLowerCase().includes('waiting')) { return 'bg-red-100 text-red-800'; } return 'bg-gray-100 text-gray-800'; }; const getPriorityColor = (priority?: string): string => { switch (priority?.toLowerCase()) { case 'highest': return 'bg-red-500 text-white'; case 'high': return 'bg-orange-500 text-white'; case 'medium': return 'bg-yellow-500 text-white'; case 'low': return 'bg-green-500 text-white'; case 'lowest': return 'bg-gray-500 text-white'; default: return 'bg-gray-300 text-gray-800'; } }; if (!sprint) return null; return (
{/* En-tête du sprint */}
{sprint.completedPoints}
Points complétés
{sprint.plannedPoints}
Points planifiés
= 80 ? 'text-green-600' : sprint.completionRate >= 60 ? 'text-orange-600' : 'text-red-600'}`}> {sprint.completionRate.toFixed(1)}%
Taux de completion
Période
{new Date(sprint.startDate).toLocaleDateString('fr-FR')} - {new Date(sprint.endDate).toLocaleDateString('fr-FR')}
{/* Onglets */}
{/* Contenu selon l'onglet */} {loading && (

Chargement des détails du sprint...

)} {error && (

❌ {error}

)} {!loading && !error && sprintDetails && ( <> {/* Vue d'ensemble */} {selectedTab === 'overview' && (

👥 Répartition par assigné

{sprintDetails.assigneeDistribution.map(assignee => (
setSelectedAssignee( selectedAssignee === assignee.displayName ? null : assignee.displayName )} > {assignee.displayName}
✅ {assignee.completedIssues} 🔄 {assignee.inProgressIssues} 📋 {assignee.totalIssues}
))}

🔄 Répartition par statut

{sprintDetails.statusDistribution.map(status => (
setSelectedStatus( selectedStatus === status.status ? null : status.status )} > {status.status}
{status.count} ({status.percentage.toFixed(1)}%)
))}
)} {/* Liste des tickets */} {selectedTab === 'issues' && (

📋 Tickets du sprint ({filteredIssues.length})

{selectedAssignee && ( 👤 {selectedAssignee} )} {selectedStatus && ( 🔄 {selectedStatus} )}
{filteredIssues.map(issue => (
{issue.key} {issue.status.name} {issue.priority && ( {issue.priority.name} )}

{issue.summary}

📋 {issue.issuetype.name} 👤 {issue.assignee?.displayName || 'Non assigné'} 📅 {new Date(issue.created).toLocaleDateString('fr-FR')}
))}
)} {/* Métriques détaillées */} {selectedTab === 'metrics' && (

📊 Métriques générales

Total tickets: {sprintDetails.metrics.totalIssues}
Tickets complétés: {sprintDetails.metrics.completedIssues}
En cours: {sprintDetails.metrics.inProgressIssues}
Cycle time moyen: {sprintDetails.metrics.averageCycleTime.toFixed(1)} jours

📈 Tendance vélocité

{sprintDetails.metrics.velocityTrend === 'up' ? '📈' : sprintDetails.metrics.velocityTrend === 'down' ? '📉' : '➡️'}

{sprintDetails.metrics.velocityTrend === 'up' ? 'En progression' : sprintDetails.metrics.velocityTrend === 'down' ? 'En baisse' : 'Stable'}

⚠️ Points d'attention

{sprint.completionRate < 70 && (
• Taux de completion faible ({sprint.completionRate.toFixed(1)}%)
)} {sprintDetails.metrics.blockedIssues > 0 && (
• {sprintDetails.metrics.blockedIssues} ticket(s) bloqué(s)
)} {sprintDetails.metrics.averageCycleTime > 14 && (
• Cycle time élevé ({sprintDetails.metrics.averageCycleTime.toFixed(1)} jours)
)} {sprint.completionRate >= 90 && sprintDetails.metrics.blockedIssues === 0 && (
• Sprint réussi sans blockers majeurs
)}
)} )} {/* Actions */}
); }