From a1f82a4c9bcd899e699f0ef057525ff0971af680 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 3 Oct 2025 08:15:12 +0200 Subject: [PATCH] feat: refactor TFS integration structure and add scheduler functionality - Updated TFS service imports to a new directory structure for better organization. - Introduced new API routes for TFS scheduler configuration and status retrieval. - Implemented TFS scheduler logic to manage automatic synchronization based on user preferences. - Added components for TFS configuration and scheduler management, enhancing user interaction with TFS settings. - Removed deprecated TfsSync component, consolidating functionality into the new structure. --- src/actions/tfs.ts | 2 +- src/app/api/tfs/delete-all/route.ts | 2 +- src/app/api/tfs/scheduler-config/route.ts | 85 ++++ src/app/api/tfs/scheduler-status/route.ts | 30 ++ src/app/api/tfs/sync/route.ts | 2 +- src/app/api/tfs/test/route.ts | 2 +- src/components/forms/task/TaskTfsInfo.tsx | 2 +- .../IntegrationsSettingsPageClient.tsx | 14 +- .../settings/{ => jira}/JiraConfigForm.tsx | 0 .../settings/{ => tfs}/TfsConfigForm.tsx | 71 +--- .../settings/tfs/TfsSchedulerConfig.tsx | 246 ++++++++++++ src/components/settings/tfs/TfsSync.tsx | 370 ++++++++++++++++++ src/components/tfs/TfsSync.tsx | 100 ----- src/lib/types.ts | 2 +- src/services/core/user-preferences.ts | 20 +- src/services/integrations/tfs/scheduler.ts | 208 ++++++++++ src/services/integrations/{ => tfs}/tfs.ts | 42 -- 17 files changed, 975 insertions(+), 223 deletions(-) create mode 100644 src/app/api/tfs/scheduler-config/route.ts create mode 100644 src/app/api/tfs/scheduler-status/route.ts rename src/components/settings/{ => jira}/JiraConfigForm.tsx (100%) rename src/components/settings/{ => tfs}/TfsConfigForm.tsx (91%) create mode 100644 src/components/settings/tfs/TfsSchedulerConfig.tsx create mode 100644 src/components/settings/tfs/TfsSync.tsx delete mode 100644 src/components/tfs/TfsSync.tsx create mode 100644 src/services/integrations/tfs/scheduler.ts rename src/services/integrations/{ => tfs}/tfs.ts (94%) diff --git a/src/actions/tfs.ts b/src/actions/tfs.ts index e4567f7..7d2e428 100644 --- a/src/actions/tfs.ts +++ b/src/actions/tfs.ts @@ -2,7 +2,7 @@ import { userPreferencesService } from '@/services/core/user-preferences'; import { revalidatePath } from 'next/cache'; -import { tfsService, TfsConfig } from '@/services/integrations/tfs'; +import { tfsService, TfsConfig } from '@/services/integrations/tfs/tfs'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; diff --git a/src/app/api/tfs/delete-all/route.ts b/src/app/api/tfs/delete-all/route.ts index a329098..01aedb9 100644 --- a/src/app/api/tfs/delete-all/route.ts +++ b/src/app/api/tfs/delete-all/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { tfsService } from '@/services/integrations/tfs'; +import { tfsService } from '@/services/integrations/tfs/tfs'; /** * Supprime toutes les tâches TFS de la base de données locale diff --git a/src/app/api/tfs/scheduler-config/route.ts b/src/app/api/tfs/scheduler-config/route.ts new file mode 100644 index 0000000..5989ccb --- /dev/null +++ b/src/app/api/tfs/scheduler-config/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { userPreferencesService } from '@/services/core/user-preferences'; +import { tfsScheduler } from '@/services/integrations/tfs/scheduler'; + +/** + * GET /api/tfs/scheduler-config + * Récupère la configuration du scheduler TFS + */ +export async function GET() { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ success: false, error: 'Non authentifié' }, { status: 401 }); + } + + const schedulerConfig = await userPreferencesService.getTfsSchedulerConfig(session.user.id); + + return NextResponse.json({ + success: true, + data: schedulerConfig + }); + } catch (error) { + console.error('Erreur récupération config scheduler TFS:', error); + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur lors de la récupération' + }, { status: 500 }); + } +} + +/** + * POST /api/tfs/scheduler-config + * Sauvegarde la configuration du scheduler TFS + */ +export async function POST(request: NextRequest) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ success: false, error: 'Non authentifié' }, { status: 401 }); + } + + const body = await request.json(); + const { tfsAutoSync, tfsSyncInterval } = body; + + if (typeof tfsAutoSync !== 'boolean') { + return NextResponse.json({ + success: false, + error: 'tfsAutoSync doit être un booléen' + }, { status: 400 }); + } + + if (!['hourly', 'daily', 'weekly'].includes(tfsSyncInterval)) { + return NextResponse.json({ + success: false, + error: 'tfsSyncInterval doit être hourly, daily ou weekly' + }, { status: 400 }); + } + + await userPreferencesService.saveTfsSchedulerConfig( + session.user.id, + tfsAutoSync, + tfsSyncInterval + ); + + // Redémarrer le scheduler avec la nouvelle configuration + await tfsScheduler.restart(session.user.id); + + // Récupérer le statut mis à jour + const status = await tfsScheduler.getStatus(session.user.id); + + return NextResponse.json({ + success: true, + message: 'Configuration scheduler TFS mise à jour', + data: status + }); + } catch (error) { + console.error('Erreur sauvegarde config scheduler TFS:', error); + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur lors de la sauvegarde' + }, { status: 500 }); + } +} diff --git a/src/app/api/tfs/scheduler-status/route.ts b/src/app/api/tfs/scheduler-status/route.ts new file mode 100644 index 0000000..ee4fa82 --- /dev/null +++ b/src/app/api/tfs/scheduler-status/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { tfsScheduler } from '@/services/integrations/tfs/scheduler'; + +/** + * GET /api/tfs/scheduler-status + * Récupère le statut du scheduler TFS + */ +export async function GET() { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ success: false, error: 'Non authentifié' }, { status: 401 }); + } + + const status = await tfsScheduler.getStatus(session.user.id); + + return NextResponse.json({ + success: true, + data: status + }); + } catch (error) { + console.error('Erreur récupération statut scheduler TFS:', error); + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur lors de la récupération' + }, { status: 500 }); + } +} diff --git a/src/app/api/tfs/sync/route.ts b/src/app/api/tfs/sync/route.ts index d07dc9f..05f5e2e 100644 --- a/src/app/api/tfs/sync/route.ts +++ b/src/app/api/tfs/sync/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { tfsService } from '@/services/integrations/tfs'; +import { tfsService } from '@/services/integrations/tfs/tfs'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; diff --git a/src/app/api/tfs/test/route.ts b/src/app/api/tfs/test/route.ts index 1313d0d..c67ae99 100644 --- a/src/app/api/tfs/test/route.ts +++ b/src/app/api/tfs/test/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { tfsService } from '@/services/integrations/tfs'; +import { tfsService } from '@/services/integrations/tfs/tfs'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; diff --git a/src/components/forms/task/TaskTfsInfo.tsx b/src/components/forms/task/TaskTfsInfo.tsx index e8fe431..0c0664a 100644 --- a/src/components/forms/task/TaskTfsInfo.tsx +++ b/src/components/forms/task/TaskTfsInfo.tsx @@ -2,7 +2,7 @@ import { Badge } from '@/components/ui/Badge'; import { Task } from '@/lib/types'; -import { TfsConfig } from '@/services/integrations/tfs'; +import { TfsConfig } from '@/services/integrations/tfs/tfs'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; interface TaskTfsInfoProps { diff --git a/src/components/settings/IntegrationsSettingsPageClient.tsx b/src/components/settings/IntegrationsSettingsPageClient.tsx index 021e62c..3b841f7 100644 --- a/src/components/settings/IntegrationsSettingsPageClient.tsx +++ b/src/components/settings/IntegrationsSettingsPageClient.tsx @@ -1,15 +1,16 @@ 'use client'; import { JiraConfig } from '@/lib/types'; -import { TfsConfig } from '@/services/integrations/tfs'; +import { TfsConfig } from '@/services/integrations/tfs/tfs'; import { Header } from '@/components/ui/Header'; import { Card, CardHeader, CardContent } from '@/components/ui/Card'; -import { JiraConfigForm } from '@/components/settings/JiraConfigForm'; +import { JiraConfigForm } from './jira/JiraConfigForm'; import { JiraSync } from '@/components/jira/JiraSync'; import { JiraLogs } from '@/components/jira/JiraLogs'; import { JiraSchedulerConfig } from '@/components/jira/JiraSchedulerConfig'; -import { TfsConfigForm } from '@/components/settings/TfsConfigForm'; -import { TfsSync } from '@/components/tfs/TfsSync'; +import { TfsConfigForm } from './tfs/TfsConfigForm'; +import { TfsSync } from './tfs/TfsSync'; +import { TfsSchedulerConfig } from './tfs/TfsSchedulerConfig'; import Link from 'next/link'; interface IntegrationsSettingsPageClientProps { @@ -154,7 +155,10 @@ export function IntegrationsSettingsPageClient({ {/* Actions TFS */}
{initialTfsConfig?.enabled ? ( - + <> + + + ) : ( diff --git a/src/components/settings/JiraConfigForm.tsx b/src/components/settings/jira/JiraConfigForm.tsx similarity index 100% rename from src/components/settings/JiraConfigForm.tsx rename to src/components/settings/jira/JiraConfigForm.tsx diff --git a/src/components/settings/TfsConfigForm.tsx b/src/components/settings/tfs/TfsConfigForm.tsx similarity index 91% rename from src/components/settings/TfsConfigForm.tsx rename to src/components/settings/tfs/TfsConfigForm.tsx index 103d6fc..559e2d2 100644 --- a/src/components/settings/TfsConfigForm.tsx +++ b/src/components/settings/tfs/TfsConfigForm.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useTransition } from 'react'; -import { TfsConfig } from '@/services/integrations/tfs'; +import { TfsConfig } from '@/services/integrations/tfs/tfs'; import { getTfsConfig, saveTfsConfig, deleteAllTfsTasks } from '@/actions/tfs'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; @@ -21,7 +21,6 @@ export function TfsConfigForm() { type: 'success' | 'error'; text: string; } | null>(null); - const [testingConnection, setTestingConnection] = useState(false); const [showForm, setShowForm] = useState(false); const [isLoading, setIsLoading] = useState(true); const [deletingTasks, setDeletingTasks] = useState(false); @@ -119,59 +118,6 @@ export function TfsConfigForm() { }); }; - const testConnection = async () => { - try { - setTestingConnection(true); - setMessage(null); - - // Sauvegarder d'abord la config - const saveResult = await saveTfsConfig(config); - if (!saveResult.success) { - setMessage({ - type: 'error', - text: saveResult.error || 'Erreur lors de la sauvegarde', - }); - return; - } - - // Attendre un peu que la configuration soit prise en compte - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // Tester la connexion avec la route dédiée - const response = await fetch('/api/tfs/test', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - const result = await response.json(); - console.log('Test TFS - Réponse:', { status: response.status, result }); - - if (response.ok && result.connected) { - setMessage({ - type: 'success', - text: `Connexion Azure DevOps réussie ! ${result.message || ''}`, - }); - } else { - const errorMessage = - result.error || result.details || 'Erreur de connexion inconnue'; - setMessage({ - type: 'error', - text: `Connexion échouée: ${errorMessage}`, - }); - console.error('Test TFS échoué:', result); - } - } catch (error) { - console.error('Erreur test connexion TFS:', error); - setMessage({ - type: 'error', - text: `Erreur réseau: ${error instanceof Error ? error.message : 'Erreur inconnue'}`, - }); - } finally { - setTestingConnection(false); - } - }; const handleDeleteAllTasks = async () => { setShowDeleteTasksConfirm(true); @@ -365,6 +311,7 @@ export function TfsConfigForm() {
)} + {/* Formulaire de configuration */} {showForm && (
- - {isTfsConfigured && ( + + + {/* Sélecteur d'intervalle */} + {schedulerStatus.isEnabled && ( +
+ Fréquence de synchronisation +
+ {(['hourly', 'daily', 'weekly'] as const).map((interval) => ( + + ))} +
+
+ )} + + + {/* Avertissement si TFS non configuré */} + {!schedulerStatus.tfsConfigured && ( +
+

+ ⚠️ Configurez d'abord votre connexion TFS pour activer la synchronisation automatique. +

+
+ )} + + + ); +} diff --git a/src/components/settings/tfs/TfsSync.tsx b/src/components/settings/tfs/TfsSync.tsx new file mode 100644 index 0000000..345bda4 --- /dev/null +++ b/src/components/settings/tfs/TfsSync.tsx @@ -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/tfs'; + +interface TfsSyncProps { + onSyncComplete?: () => void; + className?: string; +} + +export function TfsSync({ onSyncComplete, className = "" }: TfsSyncProps) { + const [isConnected, setIsConnected] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isSyncing, setIsSyncing] = useState(false); + const [lastSyncResult, setLastSyncResult] = useState(null); + const [error, setError] = useState(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 ? ( + ✓ Connecté + ) : ( + ✗ Déconnecté + ); + }; + + const getSyncStatus = () => { + if (!lastSyncResult) return null; + + const { + success, + totalPullRequests = 0, + pullRequestsCreated = 0, + pullRequestsUpdated = 0, + pullRequestsSkipped = 0, + pullRequestsDeleted = 0, + errors = [], + actions = [] + } = lastSyncResult; + + return ( +
+
+
+ 0 ? "danger" : + (totalPullRequests > 0 ? "warning" : "success")) + } size="sm"> + {success ? "✓ Succès" : + (errors.length > 0 ? "⚠ Erreurs" : + (totalPullRequests > 0 ? "✓ À jour" : "✓ Synchronisé"))} + + + {getToday().toLocaleTimeString()} + +
+
+ {totalPullRequests > 0 + ? `${totalPullRequests} PR${totalPullRequests > 1 ? 's' : ''} trouvée${totalPullRequests > 1 ? 's' : ''}` + : 'Aucune PR trouvée' + } +
+
+ +
+
+
{pullRequestsCreated}
+
Créées
+
+
+
{pullRequestsUpdated}
+
Mises à jour
+
+
+
{pullRequestsSkipped}
+
Ignorées
+
+
+
{pullRequestsDeleted}
+
Supprimées
+
+
+ + {/* Résumé textuel avec bouton détails */} +
+
+
Résumé:
+ {actions && actions.length > 0 && ( + + )} +
+
+ {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'} +
+
+ + {errors && errors.length > 0 && ( +
+
Erreurs ({errors.length}):
+
+ {errors.map((err, i) => ( +
{err}
+ ))} +
+
+ )} +
+ ); + }; + + return ( + + +
+
+
+

+ TFS SYNC +

+
+ {getConnectionStatus()} +
+
+ + + {/* Test de connexion */} +
+ + + +
+ + {/* Messages d'erreur */} + {error && ( +
+ {error} +
+ )} + + {/* Résultats de sync */} + {getSyncStatus()} + + {/* Info */} +
+
• Synchronisation unidirectionnelle (TFS → TowerControl)
+
• Les modifications locales sont préservées
+
• Seules les Pull Requests assignées sont synchronisées
+
• Les PRs fermées sont automatiquement supprimées
+
+
+ + {/* Modal détails de synchronisation */} + {lastSyncResult && ( + setShowDetails(false)} + title="📋 DÉTAILS DE SYNCHRONISATION" + size="xl" + > +
+

+ {(lastSyncResult.actions || []).length} action{(lastSyncResult.actions || []).length > 1 ? 's' : ''} effectuée{(lastSyncResult.actions || []).length > 1 ? 's' : ''} +

+ +
+ {(lastSyncResult.actions || []).length > 0 ? ( + + ) : ( +
+
📝
+
Aucun détail disponible pour cette synchronisation
+
Les détails sont disponibles pour les nouvelles synchronisations
+
+ )} +
+
+
+ )} +
+ ); +} + +// 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); + + return ( +
+ {Object.entries(groupedActions).map(([type, typeActions]) => ( +
+

+ {getActionIcon(type as TfsSyncAction['type'])} + {getActionLabel(type as TfsSyncAction['type'])} ({typeActions.length}) +

+ +
+ {typeActions.map((action, index) => ( +
+
+
+
+ + PR #{action.pullRequestId} + + + {action.prTitle} + +
+
+ + {getActionLabel(action.type)} + +
+ + {action.reason && ( +
+ 💡 {action.reason} +
+ )} + + {action.changes && action.changes.length > 0 && ( +
+
+ Modifications: +
+ {action.changes.map((change, changeIndex) => ( +
+ {change} +
+ ))} +
+ )} +
+ ))} +
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/tfs/TfsSync.tsx b/src/components/tfs/TfsSync.tsx deleted file mode 100644 index 646f37b..0000000 --- a/src/components/tfs/TfsSync.tsx +++ /dev/null @@ -1,100 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; -import { Card, CardHeader, CardContent } from '@/components/ui/Card'; -import { syncTfsPullRequests } from '@/actions/tfs'; - -export function TfsSync() { - const [isPending, startTransition] = useTransition(); - const [lastSync, setLastSync] = useState<{ - success: boolean; - message: string; - stats?: { - created: number; - updated: number; - skipped: number; - deleted: number; - } - } | null>(null); - - const handleSync = () => { - startTransition(async () => { - setLastSync(null); - - const result = await syncTfsPullRequests(); - - if (result.success) { - setLastSync({ - success: true, - message: result.message || 'Synchronisation réussie', - stats: result.data ? { - created: result.data.pullRequestsCreated, - updated: result.data.pullRequestsUpdated, - skipped: result.data.pullRequestsSkipped, - deleted: result.data.pullRequestsDeleted - } : undefined - }); - } else { - setLastSync({ - success: false, - message: result.error || 'Erreur lors de la synchronisation' - }); - } - }); - }; - - return ( - - -

- 🔄 - Synchronisation TFS -

-
- -

- Synchronise manuellement les Pull Requests depuis Azure DevOps -

- - {/* Résultat de la dernière synchronisation */} - {lastSync && ( -
-
- {lastSync.success ? '✅' : '❌'} {lastSync.message} -
- {lastSync.stats && ( -
- Créées: {lastSync.stats.created} | - Mises à jour: {lastSync.stats.updated} | - Ignorées: {lastSync.stats.skipped} | - Supprimées: {lastSync.stats.deleted} -
- )} -
- )} - - - -
- Les Pull Requests seront importées comme tâches dans le tableau Kanban -
-
-
- ); -} diff --git a/src/lib/types.ts b/src/lib/types.ts index 7deefce..a4c6e0d 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,4 @@ -import { TfsConfig } from '@/services/integrations/tfs'; +import { TfsConfig } from '@/services/integrations/tfs/tfs'; import { Theme } from './theme-config'; // Réexporter Theme pour les autres modules diff --git a/src/services/core/user-preferences.ts b/src/services/core/user-preferences.ts index d52311f..bf3f476 100644 --- a/src/services/core/user-preferences.ts +++ b/src/services/core/user-preferences.ts @@ -7,7 +7,7 @@ import { JiraConfig, Theme, } from '@/lib/types'; -import { TfsConfig } from '@/services/integrations/tfs'; +import { TfsConfig } from '@/services/integrations/tfs/tfs'; import { prisma } from './database'; import { getConfig } from '@/lib/config'; @@ -94,6 +94,7 @@ class UserPreferencesService { // S'assurer que les nouveaux champs existent (migration douce) await this.ensureJiraSchedulerFields(userId); + await this.ensureTfsSchedulerFields(userId); return userPrefs; } @@ -115,6 +116,23 @@ class UserPreferencesService { } } + /** + * S'assure que les champs tfsAutoSync et tfsSyncInterval existent + */ + private async ensureTfsSchedulerFields(userId: string): Promise { + try { + await prisma.$executeRaw` + UPDATE user_preferences + SET tfsAutoSync = COALESCE(tfsAutoSync, ${DEFAULT_PREFERENCES.tfsAutoSync}), + tfsSyncInterval = COALESCE(tfsSyncInterval, ${DEFAULT_PREFERENCES.tfsSyncInterval}) + WHERE userId = ${userId} + `; + } catch (error) { + // Ignorer les erreurs si les colonnes n'existent pas encore + console.debug('Migration douce des champs scheduler TFS:', error); + } + } + // === FILTRES KANBAN === /** diff --git a/src/services/integrations/tfs/scheduler.ts b/src/services/integrations/tfs/scheduler.ts new file mode 100644 index 0000000..c41b87d --- /dev/null +++ b/src/services/integrations/tfs/scheduler.ts @@ -0,0 +1,208 @@ +import { userPreferencesService } from '@/services/core/user-preferences'; +import { TfsService } from './tfs'; +import { addMinutes, getToday } from '@/lib/date-utils'; + +export interface TfsSchedulerConfig { + enabled: boolean; + interval: 'hourly' | 'daily' | 'weekly'; +} + +export class TfsScheduler { + private timer: NodeJS.Timeout | null = null; + private isRunning = false; + private currentUserId: string | null = null; + + /** + * Démarre le planificateur de synchronisation TFS automatique + */ + async start(userId?: string): Promise { + if (this.isRunning) { + console.log('⚠️ TFS scheduler is already running'); + return; + } + + const targetUserId = userId || 'default'; + const config = await this.getConfig(targetUserId); + + if (!config.enabled) { + console.log('📋 Automatic TFS sync is disabled'); + return; + } + + // Vérifier que TFS est configuré + const tfsConfig = await userPreferencesService.getTfsConfig(targetUserId); + if (!tfsConfig.enabled || !tfsConfig.organizationUrl || !tfsConfig.personalAccessToken) { + console.log('⚠️ TFS not configured, scheduler cannot start'); + return; + } + + const intervalMs = this.getIntervalMs(config.interval); + + // Première synchronisation immédiate (optionnelle) + // this.performScheduledSync(); + + // Planifier les synchronisations suivantes + this.timer = setInterval(() => { + this.performScheduledSync(targetUserId); + }, intervalMs); + + this.isRunning = true; + this.currentUserId = targetUserId; + console.log(`✅ TFS scheduler started with ${config.interval} interval for user ${targetUserId}`); + } + + /** + * Arrête le planificateur + */ + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + + this.isRunning = false; + this.currentUserId = null; + console.log('🛑 TFS scheduler stopped'); + } + + /** + * Redémarre le planificateur (utile lors des changements de config) + */ + async restart(userId?: string): Promise { + this.stop(); + await this.start(userId); + } + + /** + * Vérifie si le planificateur fonctionne + */ + isActive(): boolean { + return this.isRunning && this.timer !== null; + } + + /** + * Effectue une synchronisation planifiée + */ + private async performScheduledSync(userId: string = 'default'): Promise { + try { + console.log('🔄 Starting scheduled TFS sync...'); + + // Récupérer la config TFS + const tfsConfig = await userPreferencesService.getTfsConfig(userId); + + if (!tfsConfig.enabled || !tfsConfig.organizationUrl || !tfsConfig.personalAccessToken) { + console.log('⚠️ TFS config incomplete, skipping scheduled sync'); + return; + } + + // Créer le service TFS + const tfsService = new TfsService({ + enabled: tfsConfig.enabled, + organizationUrl: tfsConfig.organizationUrl, + projectName: tfsConfig.projectName, + personalAccessToken: tfsConfig.personalAccessToken, + repositories: tfsConfig.repositories || [], + ignoredRepositories: tfsConfig.ignoredRepositories || [] + }); + + // Tester la connexion d'abord + const connectionOk = await tfsService.testConnection(); + if (!connectionOk) { + console.error('❌ Scheduled TFS sync failed: connection error'); + return; + } + + // Effectuer la synchronisation + const result = await tfsService.syncTasks(); + + if (result.success) { + console.log(`✅ Scheduled TFS sync completed: ${result.pullRequestsCreated} created, ${result.pullRequestsUpdated} updated, ${result.pullRequestsSkipped} skipped`); + } else { + console.error(`❌ Scheduled TFS sync failed: ${result.errors.join(', ')}`); + } + } catch (error) { + console.error('❌ Scheduled TFS sync error:', error); + } + } + + /** + * Convertit l'intervalle en millisecondes + */ + private getIntervalMs(interval: TfsSchedulerConfig['interval']): number { + const intervals = { + hourly: 60 * 60 * 1000, // 1 heure + daily: 24 * 60 * 60 * 1000, // 24 heures + weekly: 7 * 24 * 60 * 60 * 1000, // 7 jours + }; + + return intervals[interval]; + } + + /** + * Obtient le prochain moment de synchronisation + */ + async getNextSyncTime(): Promise { + if (!this.isRunning || !this.timer) { + return null; + } + + const config = await this.getConfig(); + const intervalMs = this.getIntervalMs(config.interval); + + return addMinutes(getToday(), Math.floor(intervalMs / (1000 * 60))); + } + + /** + * Récupère la configuration du scheduler depuis les user preferences + */ + private async getConfig(userId: string = 'default'): Promise { + try { + const [tfsConfig, schedulerConfig] = await Promise.all([ + userPreferencesService.getTfsConfig(userId), + userPreferencesService.getTfsSchedulerConfig(userId) + ]); + + return { + enabled: schedulerConfig.tfsAutoSync && + tfsConfig.enabled && + !!tfsConfig.organizationUrl && + !!tfsConfig.personalAccessToken, + interval: schedulerConfig.tfsSyncInterval + }; + } catch (error) { + console.error('Error getting TFS scheduler config:', error); + return { + enabled: false, + interval: 'daily' + }; + } + } + + /** + * Obtient les stats du planificateur + */ + async getStatus(userId?: string) { + const targetUserId = userId || 'default'; + const config = await this.getConfig(targetUserId); + const tfsConfig = await userPreferencesService.getTfsConfig(targetUserId); + + return { + isRunning: this.isRunning, + isEnabled: config.enabled, + interval: config.interval, + nextSync: await this.getNextSyncTime(), + tfsConfigured: !!(tfsConfig.organizationUrl && tfsConfig.personalAccessToken), + }; + } +} + +// Instance singleton +export const tfsScheduler = new TfsScheduler(); + +// Auto-start du scheduler +// Démarrer avec un délai pour laisser l'app s'initialiser +setTimeout(() => { + console.log('🚀 Auto-starting TFS scheduler...'); + tfsScheduler.start(); +}, 8000); // 8 secondes, après le Jira scheduler + diff --git a/src/services/integrations/tfs.ts b/src/services/integrations/tfs/tfs.ts similarity index 94% rename from src/services/integrations/tfs.ts rename to src/services/integrations/tfs/tfs.ts index 58b7768..1f88a4e 100644 --- a/src/services/integrations/tfs.ts +++ b/src/services/integrations/tfs/tfs.ts @@ -205,19 +205,12 @@ export class TfsService { */ async getMyPullRequests(): Promise { try { - console.log("🔍 Récupération des PRs créées par l'utilisateur..."); - // Uniquement les PRs créées par l'utilisateur (simplifié) const createdPrs = await this.getPullRequestsByCreator(); - console.log(`👤 ${createdPrs.length} PR(s) créées par l'utilisateur`); // Filtrer les PRs selon la configuration const filteredPrs = this.filterPullRequests(createdPrs); - console.log( - `🎫 ${filteredPrs.length} PR(s) après filtrage de configuration` - ); - return filteredPrs; } catch (error) { console.error('❗️ Erreur récupération PRs utilisateur:', error); @@ -264,7 +257,6 @@ export class TfsService { const data = await response.json(); const prs = data.value || []; - console.log(`🚀 ${prs.length} PR(s) créée(s) par l'utilisateur`); return prs; } catch (error) { console.error('❗️ Erreur récupération PRs créateur:', error); @@ -426,30 +418,18 @@ export class TfsService { if (pr.closedDate) { const closedDate = parseDate(pr.closedDate); const isRecent = closedDate >= thirtyDaysAgo; - console.log( - `📅 PR ${pr.pullRequestId} (${pr.title}): Completed ${formatDateForDisplay(closedDate)} - ${isRecent ? 'INCLUSE (récente)' : 'EXCLUE (âgée)'}` - ); return isRecent; } else { // Si pas de date de fermeture, on l'inclut par sécurité - console.log( - `❓ PR ${pr.pullRequestId} (${pr.title}): Completed sans date - INCLUSE` - ); return true; } case 'abandoned': // PRs abandonnées ne sont pas pertinentes - console.log( - `❌ PR ${pr.pullRequestId} (${pr.title}): Abandoned - EXCLUE` - ); return false; default: // Statut inconnu, on l'inclut par précaution - console.log( - `❓ PR ${pr.pullRequestId} (${pr.title}): Statut inconnu "${pr.status}" - INCLUSE` - ); return true; } }); @@ -555,10 +535,7 @@ export class TfsService { const allPullRequests = await this.getMyPullRequests(); result.totalPullRequests = allPullRequests.length; - console.log(`📋 ${allPullRequests.length} Pull Requests trouvées`); - if (allPullRequests.length === 0) { - console.log('ℹ️ Aucune PR assignée trouvée'); return result; } @@ -637,8 +614,6 @@ export class TfsService { // Assigner le tag TFS await this.assignTfsTag(newTask.id); - console.log(`➡️ Nouvelle tâche créée: PR-${pullRequestId}`); - return { type: 'created', pullRequestId, @@ -662,8 +637,6 @@ export class TfsService { } if (changes.length === 0) { - console.log(`⏭️ Aucun changement pour PR-${pullRequestId}`); - // S'assurer que le tag TFS est assigné (pour les anciennes tâches) await this.assignTfsTag(existingTask.id); @@ -684,8 +657,6 @@ export class TfsService { }, }); - console.log(`🔄 Tâche mise à jour: PR-${pullRequestId} (${changes.length} changements)`); - return { type: 'updated', pullRequestId, @@ -891,25 +862,17 @@ export class TfsService { }, }); - console.log(`📋 ${existingTfsTasks.length} tâches TFS existantes`); - // Identifier les tâches à supprimer const tasksToDelete = existingTfsTasks.filter((task) => { const prId = task.tfsPullRequestId; if (!prId) { - console.log(`🤷 Tâche ${task.id} sans PR ID - à supprimer`); return true; } const shouldKeep = currentPrIds.has(prId); - if (!shouldKeep) { - console.log(`❌ PR ${prId} plus active - à supprimer`); - } return !shouldKeep; }); - console.log(`🗑️ ${tasksToDelete.length} tâches à supprimer`); - // Supprimer les tâches obsolètes for (const task of tasksToDelete) { try { @@ -922,7 +885,6 @@ export class TfsService { reason: 'Pull Request plus active ou supprimée', }); - console.log(`🗑️ Supprimé: ${task.title}`); } catch (error) { console.error(`❌ Erreur suppression tâche ${task.id}:`, error); // Continue avec les autres tâches @@ -948,16 +910,12 @@ export class TfsService { error?: string; }> { try { - console.log('🗑️ Début suppression de toutes les tâches TFS...'); - // Récupérer toutes les tâches TFS const tfsTasks = await prisma.task.findMany({ where: { source: 'tfs' }, select: { id: true, title: true }, }); - console.log(`📋 ${tfsTasks.length} tâches TFS trouvées`); - if (tfsTasks.length === 0) { return { success: true,