Files
towercontrol/src/components/tfs/TfsSync.tsx
Julien Froidefond 2137da2ac2 feat: refine TFS scheduler and user-specific configurations
- Enhanced TFS scheduler logic to better manage user-specific settings and preferences.
- Updated API routes for improved handling of user-specific configurations in TFS operations.
- Cleaned up related components to streamline user interactions and ensure accurate task synchronization.
2025-10-03 08:17:48 +02:00

370 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { getToday } from '@/lib/date-utils';
import { Modal } from '@/components/ui/Modal';
import { TfsSyncResult, TfsSyncAction } from '@/services/integrations/tfs';
interface TfsSyncProps {
onSyncComplete?: () => void;
className?: string;
}
export function TfsSync({ onSyncComplete, className = "" }: TfsSyncProps) {
const [isConnected, setIsConnected] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const [lastSyncResult, setLastSyncResult] = useState<TfsSyncResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [showDetails, setShowDetails] = useState(false);
const testConnection = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/tfs/test');
const result = await response.json();
setIsConnected(result.connected);
if (!result.connected) {
setError(result.error || result.message);
}
} catch (err) {
setIsConnected(false);
setError(err instanceof Error ? err.message : 'Erreur de connexion');
} finally {
setIsLoading(false);
}
};
const startSync = async () => {
setIsSyncing(true);
setError(null);
try {
const response = await fetch('/api/tfs/sync', { method: 'POST' });
const result = await response.json();
console.log('TFS Sync API Response:', result);
// L'API retourne { message: '...', data: result } ou { error: '...', data: result }
const syncResult = result.data || result;
console.log('TFS Sync Result:', syncResult);
setLastSyncResult(syncResult);
// Considérer comme succès si on a une réponse (même avec status 207)
if (response.ok || response.status === 207) {
onSyncComplete?.();
}
} catch (err) {
console.error('TFS Sync Error:', err);
setError(err instanceof Error ? err.message : 'Erreur de synchronisation');
} finally {
setIsSyncing(false);
}
};
const getConnectionStatus = () => {
if (isConnected === null) return null;
return isConnected ? (
<Badge variant="success" size="sm"> Connecté</Badge>
) : (
<Badge variant="danger" size="sm"> Déconnecté</Badge>
);
};
const getSyncStatus = () => {
if (!lastSyncResult) return null;
const {
success,
totalPullRequests = 0,
pullRequestsCreated = 0,
pullRequestsUpdated = 0,
pullRequestsSkipped = 0,
pullRequestsDeleted = 0,
errors = [],
actions = []
} = lastSyncResult;
return (
<div className="space-y-3 text-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant={
success ? "success" :
(errors.length > 0 ? "danger" :
(totalPullRequests > 0 ? "warning" : "success"))
} size="sm">
{success ? "✓ Succès" :
(errors.length > 0 ? "⚠ Erreurs" :
(totalPullRequests > 0 ? "✓ À jour" : "✓ Synchronisé"))}
</Badge>
<span className="text-[var(--muted-foreground)] text-xs">
{getToday().toLocaleTimeString()}
</span>
</div>
<div className="text-xs text-[var(--muted-foreground)]">
{totalPullRequests > 0
? `${totalPullRequests} PR${totalPullRequests > 1 ? 's' : ''} trouvée${totalPullRequests > 1 ? 's' : ''}`
: 'Aucune PR trouvée'
}
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-emerald-400">{pullRequestsCreated}</div>
<div className="text-[var(--muted-foreground)]">Créées</div>
</div>
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-blue-400">{pullRequestsUpdated}</div>
<div className="text-[var(--muted-foreground)]">Mises à jour</div>
</div>
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-orange-400">{pullRequestsSkipped}</div>
<div className="text-[var(--muted-foreground)]">Ignorées</div>
</div>
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-red-400">{pullRequestsDeleted}</div>
<div className="text-[var(--muted-foreground)]">Supprimées</div>
</div>
</div>
{/* Résumé textuel avec bouton détails */}
<div className="p-2 bg-[var(--muted)]/5 rounded text-xs">
<div className="flex items-center justify-between mb-1">
<div className="font-medium text-[var(--muted-foreground)]">Résumé:</div>
{actions && actions.length > 0 && (
<Button
onClick={() => setShowDetails(true)}
variant="secondary"
size="sm"
className="text-xs px-2 py-1 h-auto"
>
Voir détails ({actions.length})
</Button>
)}
</div>
<div className="text-[var(--muted-foreground)]">
{pullRequestsCreated > 0 && `${pullRequestsCreated} nouvelle${pullRequestsCreated > 1 ? 's' : ''}`}
{pullRequestsUpdated > 0 && `${pullRequestsUpdated} mise${pullRequestsUpdated > 1 ? 's' : ''} à jour • `}
{pullRequestsDeleted > 0 && `${pullRequestsDeleted} supprimée${pullRequestsDeleted > 1 ? 's' : ''} (fermées) • `}
{pullRequestsSkipped > 0 && `${pullRequestsSkipped} déjà synchronisée${pullRequestsSkipped > 1 ? 's' : ''}`}
{(pullRequestsCreated + pullRequestsUpdated + pullRequestsDeleted + pullRequestsSkipped) === 0 && 'Aucune PR trouvée'}
</div>
</div>
{errors && errors.length > 0 && (
<div className="p-2 bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded text-xs">
<div className="font-semibold text-[var(--destructive)] mb-1">Erreurs ({errors.length}):</div>
<div className="space-y-1 max-h-20 overflow-y-auto">
{errors.map((err, i) => (
<div key={i} className="text-[var(--destructive)] font-mono text-xs">{err}</div>
))}
</div>
</div>
)}
</div>
);
};
return (
<Card className={`${className}`}>
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full animate-pulse" style={{ backgroundColor: 'var(--purple)' }}></div>
<h3 className="font-mono text-sm font-bold text-purple-400 uppercase tracking-wider">
TFS SYNC
</h3>
</div>
{getConnectionStatus()}
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Test de connexion */}
<div className="flex gap-2">
<Button
onClick={testConnection}
disabled={isLoading}
variant="secondary"
size="sm"
className="flex-1"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-3 h-3 border border-[var(--muted-foreground)] border-t-transparent rounded-full animate-spin"></div>
Test...
</div>
) : (
'Tester connexion'
)}
</Button>
<Button
onClick={startSync}
disabled={isSyncing || isConnected === false}
variant="primary"
size="sm"
className="flex-1"
>
{isSyncing ? (
<div className="flex items-center gap-2">
<div className="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin"></div>
Sync...
</div>
) : (
'🔄 Synchroniser'
)}
</Button>
</div>
{/* Messages d'erreur */}
{error && (
<div className="p-3 bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded text-sm text-[var(--destructive)]">
{error}
</div>
)}
{/* Résultats de sync */}
{getSyncStatus()}
{/* Info */}
<div className="text-xs text-[var(--muted-foreground)] space-y-1">
<div> Synchronisation unidirectionnelle (TFS TowerControl)</div>
<div> Les modifications locales sont préservées</div>
<div> Seules les Pull Requests assignées sont synchronisées</div>
<div> Les PRs fermées sont automatiquement supprimées</div>
</div>
</CardContent>
{/* Modal détails de synchronisation */}
{lastSyncResult && (
<Modal
isOpen={showDetails}
onClose={() => setShowDetails(false)}
title="📋 DÉTAILS DE SYNCHRONISATION"
size="xl"
>
<div className="space-y-4">
<p className="text-sm text-[var(--muted-foreground)]">
{(lastSyncResult.actions || []).length} action{(lastSyncResult.actions || []).length > 1 ? 's' : ''} effectuée{(lastSyncResult.actions || []).length > 1 ? 's' : ''}
</p>
<div className="max-h-[60vh] overflow-y-auto">
{(lastSyncResult.actions || []).length > 0 ? (
<SyncActionsList actions={lastSyncResult.actions || []} />
) : (
<div className="text-center py-8 text-[var(--muted-foreground)]">
<div className="text-2xl mb-2">📝</div>
<div>Aucun détail disponible pour cette synchronisation</div>
<div className="text-sm mt-1">Les détails sont disponibles pour les nouvelles synchronisations</div>
</div>
)}
</div>
</div>
</Modal>
)}
</Card>
);
}
// Composant pour afficher la liste des actions
function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) {
const getActionIcon = (type: TfsSyncAction['type']) => {
switch (type) {
case 'created': return '';
case 'updated': return '🔄';
case 'skipped': return '⏭️';
case 'deleted': return '🗑️';
default: return '❓';
}
};
const getActionColor = (type: TfsSyncAction['type']) => {
switch (type) {
case 'created': return 'text-emerald-400';
case 'updated': return 'text-blue-400';
case 'skipped': return 'text-orange-400';
case 'deleted': return 'text-red-400';
default: return 'text-gray-400';
}
};
const getActionLabel = (type: TfsSyncAction['type']) => {
switch (type) {
case 'created': return 'Créée';
case 'updated': return 'Mise à jour';
case 'skipped': return 'Ignorée';
case 'deleted': return 'Supprimée';
default: return 'Inconnue';
}
};
// Grouper les actions par type
const groupedActions = actions.reduce((acc, action) => {
if (!acc[action.type]) acc[action.type] = [];
acc[action.type].push(action);
return acc;
}, {} as Record<string, TfsSyncAction[]>);
return (
<div className="space-y-6">
{Object.entries(groupedActions).map(([type, typeActions]) => (
<div key={type} className="space-y-3">
<h4 className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as TfsSyncAction['type'])}`}>
{getActionIcon(type as TfsSyncAction['type'])}
{getActionLabel(type as TfsSyncAction['type'])} ({typeActions.length})
</h4>
<div className="space-y-2">
{typeActions.map((action, index) => (
<div key={index} className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2">
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
PR #{action.pullRequestId}
</span>
<span className="text-sm text-[var(--muted-foreground)] truncate">
{action.prTitle}
</span>
</div>
</div>
<Badge variant="outline" size="sm" className="shrink-0">
{getActionLabel(action.type)}
</Badge>
</div>
{action.reason && (
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
💡 {action.reason}
</div>
)}
{action.changes && action.changes.length > 0 && (
<div className="mt-1 space-y-0.5">
<div className="text-xs font-medium text-[var(--muted-foreground)]">
Modifications:
</div>
{action.changes.map((change, changeIndex) => (
<div key={changeIndex} className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-purple-400/30">
{change}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
))}
</div>
);
}