feat: TFS Sync

This commit is contained in:
Julien Froidefond
2025-09-22 21:51:12 +02:00
parent 472135a97f
commit 723a44df32
27 changed files with 3309 additions and 364 deletions

View File

@@ -8,9 +8,10 @@ import { prisma } from './database';
import { parseDate, formatDateForDisplay } from '@/lib/date-utils';
export interface JiraConfig {
baseUrl: string;
email: string;
apiToken: string;
enabled: boolean;
baseUrl?: string;
email?: string;
apiToken?: string;
projectKey?: string; // Clé du projet à surveiller pour les analytics d'équipe (ex: "MYTEAM")
ignoredProjects?: string[]; // Liste des clés de projets à ignorer (ex: ["DEMO", "TEST"])
}
@@ -23,6 +24,27 @@ export interface JiraSyncAction {
changes?: string[]; // Liste des champs modifiés pour les updates
}
// Type générique pour compatibilité avec d'autres services
export interface SyncAction {
type: 'created' | 'updated' | 'skipped' | 'deleted';
itemId: string | number;
title: string;
message?: string;
}
export interface SyncResult {
success: boolean;
totalItems: number;
actions: SyncAction[];
errors: string[];
stats: {
created: number;
updated: number;
skipped: number;
deleted: number;
};
}
export interface JiraSyncResult {
success: boolean;
tasksFound: number;
@@ -35,7 +57,7 @@ export interface JiraSyncResult {
}
export class JiraService {
private config: JiraConfig;
readonly config: JiraConfig;
constructor(config: JiraConfig) {
this.config = config;
@@ -89,6 +111,32 @@ export class JiraService {
}
}
/**
* Valide la configuration Jira
*/
async validateConfig(): Promise<{ valid: boolean; error?: string }> {
if (!this.config.enabled) {
return { valid: false, error: 'Jira désactivé' };
}
if (!this.config.baseUrl) {
return { valid: false, error: 'URL de base Jira manquante' };
}
if (!this.config.email) {
return { valid: false, error: 'Email Jira manquant' };
}
if (!this.config.apiToken) {
return { valid: false, error: 'Token API Jira manquant' };
}
// Tester la connexion pour validation complète
const connectionOk = await this.testConnection();
if (!connectionOk) {
return { valid: false, error: 'Impossible de se connecter avec ces paramètres' };
}
return { valid: true };
}
/**
* Filtre les tâches Jira selon les projets ignorés
*/
@@ -245,17 +293,18 @@ export class JiraService {
/**
* Synchronise les tickets Jira avec la base locale
*/
async syncTasks(): Promise<JiraSyncResult> {
const result: JiraSyncResult = {
async syncTasks(): Promise<SyncResult> {
const result: SyncResult = {
success: false,
tasksFound: 0,
tasksCreated: 0,
tasksUpdated: 0,
tasksSkipped: 0,
tasksDeleted: 0,
totalItems: 0,
actions: [],
errors: [],
actions: []
stats: { created: 0, updated: 0, skipped: 0, deleted: 0 }
};
// Variables locales pour compatibilité avec l'ancien code
let tasksDeleted = 0;
const jiraActions: JiraSyncAction[] = [];
try {
console.log('🔄 Début de la synchronisation Jira...');
@@ -265,7 +314,7 @@ export class JiraService {
// Récupérer les tickets Jira actuellement assignés
const jiraTasks = await this.getAssignedIssues();
result.tasksFound = jiraTasks.length;
result.totalItems = jiraTasks.length;
console.log(`📋 ${jiraTasks.length} tickets trouvés dans Jira`);
@@ -281,16 +330,25 @@ export class JiraService {
try {
const syncAction = await this.syncSingleTask(jiraTask);
// Convertir JiraSyncAction vers SyncAction
const standardAction: SyncAction = {
type: syncAction.type,
itemId: syncAction.taskKey,
title: syncAction.taskTitle,
message: syncAction.reason || syncAction.changes?.join('; ')
};
// Ajouter l'action au résultat
result.actions.push(syncAction);
result.actions.push(standardAction);
jiraActions.push(syncAction);
// Compter les actions
if (syncAction.type === 'created') {
result.tasksCreated++;
result.stats.created++;
} else if (syncAction.type === 'updated') {
result.tasksUpdated++;
result.stats.updated++;
} else {
result.tasksSkipped++;
result.stats.skipped++;
}
} catch (error) {
console.error(`Erreur sync ticket ${jiraTask.key}:`, error);
@@ -300,8 +358,19 @@ export class JiraService {
// Nettoyer les tâches Jira qui ne sont plus assignées à l'utilisateur
const deletedActions = await this.cleanupUnassignedTasks(currentJiraIds);
result.tasksDeleted = deletedActions.length;
result.actions.push(...deletedActions);
tasksDeleted = deletedActions.length;
result.stats.deleted = tasksDeleted;
// Convertir les actions de suppression
for (const action of deletedActions) {
const standardAction: SyncAction = {
type: 'deleted',
itemId: action.taskKey,
title: action.taskTitle,
message: action.reason
};
result.actions.push(standardAction);
}
// Déterminer le succès et enregistrer le log
result.success = result.errors.length === 0;
@@ -721,14 +790,14 @@ export class JiraService {
/**
* Enregistre un log de synchronisation
*/
private async logSync(result: JiraSyncResult): Promise<void> {
private async logSync(result: SyncResult): Promise<void> {
try {
await prisma.syncLog.create({
data: {
source: 'jira',
status: result.success ? 'success' : 'error',
message: result.errors.length > 0 ? result.errors.join('; ') : null,
tasksSync: result.tasksCreated + result.tasksUpdated
tasksSync: result.stats.created + result.stats.updated
}
});
} catch (error) {
@@ -750,5 +819,10 @@ export function createJiraService(): JiraService | null {
return null;
}
return new JiraService({ baseUrl, email, apiToken });
return new JiraService({
enabled: true,
baseUrl,
email,
apiToken
});
}