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.
This commit is contained in:
370
src/components/tfs/TfsSync.tsx
Normal file
370
src/components/tfs/TfsSync.tsx
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
1076
src/services/integrations/tfs.ts
Normal file
1076
src/services/integrations/tfs.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user