diff --git a/src/app/api/jira/sync/route.ts b/src/app/api/jira/sync/route.ts index 21c36a4..de8f189 100644 --- a/src/app/api/jira/sync/route.ts +++ b/src/app/api/jira/sync/route.ts @@ -118,6 +118,17 @@ export async function POST(request: Request) { const syncResult = await jiraService.syncTasks(session.user.id); // Convertir SyncResult en JiraSyncResult pour le client + // Utiliser les actions Jira originales si disponibles pour préserver les détails (changes, etc.) + const actions = + syncResult.jiraActions || + syncResult.actions.map((action) => ({ + type: action.type as 'created' | 'updated' | 'skipped' | 'deleted', + taskKey: action.itemId.toString(), + taskTitle: action.title, + reason: action.message, + changes: action.message ? [action.message] : undefined, + })); + const jiraSyncResult = { success: syncResult.success, tasksFound: syncResult.totalItems, @@ -127,13 +138,7 @@ export async function POST(request: Request) { tasksDeleted: syncResult.stats.deleted, errors: syncResult.errors, unknownStatuses: syncResult.unknownStatuses || [], // Nouveaux statuts inconnus - actions: syncResult.actions.map((action) => ({ - type: action.type as 'created' | 'updated' | 'skipped' | 'deleted', - taskKey: action.itemId.toString(), - taskTitle: action.title, - reason: action.message, - changes: action.message ? [action.message] : undefined, - })), + actions, }; if (syncResult.success) { diff --git a/src/components/jira/JiraSync.tsx b/src/components/jira/JiraSync.tsx index 6b73b63..b846bac 100644 --- a/src/components/jira/JiraSync.tsx +++ b/src/components/jira/JiraSync.tsx @@ -395,66 +395,215 @@ function SyncActionsList({ actions }: { actions: JiraSyncAction[] }) { {} as Record ); + // Ordre d'affichage : créées, mises à jour, supprimées, ignorées (toujours en dernier) + const displayOrder: JiraSyncAction['type'][] = [ + 'created', + 'updated', + 'deleted', + 'skipped', + ]; + return (
- {Object.entries(groupedActions).map(([type, typeActions]) => ( -
-

- {getActionIcon(type as JiraSyncAction['type'])} - {getActionLabel(type as JiraSyncAction['type'])} ( - {typeActions.length}) -

+ {displayOrder + .filter((type) => groupedActions[type]?.length > 0) + .map((type) => { + const typeActions = groupedActions[type]; -
- {typeActions.map((action, index) => ( -
-
-
-
- - {action.taskKey} - - - {action.taskTitle} + // Pour les actions skipped, séparer celles avec champs préservés de celles sans changement + if (type === 'skipped') { + const withPreservedFields = typeActions.filter( + (action) => action.changes && action.changes.length > 0 + ); + const withoutChanges = typeActions.filter( + (action) => !action.changes || action.changes.length === 0 + ); + + return ( +
+

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

+ + {/* Tâches avec champs préservés */} + {withPreservedFields.length > 0 && ( +
+
+
+ + Avec champs préservés ({withPreservedFields.length}) +
-
- - {getActionLabel(action.type)} - -
+
+ {withPreservedFields.map((action, index) => ( +
+
+
+
+ + {action.taskKey} + + + {action.taskTitle} + +
+
+ + {getActionLabel(action.type)} + +
- {action.reason && ( -
- {action.reason} + {action.reason && ( +
+ {action.reason} +
+ )} + + {action.changes && action.changes.length > 0 && ( +
+
+ Champs préservés: +
+ {action.changes.map((change, changeIndex) => ( +
+ {change} +
+ ))} +
+ )} +
+ ))} +
)} - {action.changes && action.changes.length > 0 && ( -
-
- Modifications: -
- {action.changes.map((change, changeIndex) => ( -
- {change} + {/* Tâches sans changement */} + {withoutChanges.length > 0 && ( +
+ {withPreservedFields.length > 0 && ( +
+
+ + Aucun changement ({withoutChanges.length}) + +
- ))} + )} +
+ {withoutChanges.map((action, index) => ( +
+
+
+
+ + {action.taskKey} + + + {action.taskTitle} + +
+
+ + {getActionLabel(action.type)} + +
+ + {action.reason && ( +
+ {action.reason} +
+ )} +
+ ))} +
)}
- ))} -
-
- ))} + ); + } + + // Pour les autres types, affichage normal + return ( +
+

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

+ +
+ {typeActions.map((action, index) => ( +
+
+
+
+ + {action.taskKey} + + + {action.taskTitle} + +
+
+ + {getActionLabel(action.type)} + +
+ + {action.reason && ( +
+ {action.reason} +
+ )} + + {action.changes && action.changes.length > 0 && ( +
+
+ Modifications: +
+ {action.changes.map((change, changeIndex) => ( +
+ {change} +
+ ))} +
+ )} +
+ ))} +
+
+ ); + })}
); } diff --git a/src/components/settings/tfs/TfsSync.tsx b/src/components/settings/tfs/TfsSync.tsx index 2afa055..d6799f1 100644 --- a/src/components/settings/tfs/TfsSync.tsx +++ b/src/components/settings/tfs/TfsSync.tsx @@ -383,66 +383,215 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) { {} as Record ); + // Ordre d'affichage : créées, mises à jour, supprimées, ignorées (toujours en dernier) + const displayOrder: TfsSyncAction['type'][] = [ + 'created', + 'updated', + 'deleted', + 'skipped', + ]; + return (
- {Object.entries(groupedActions).map(([type, typeActions]) => ( -
-

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

+ {displayOrder + .filter((type) => groupedActions[type]?.length > 0) + .map((type) => { + const typeActions = groupedActions[type] as TfsSyncAction[]; -
- {typeActions.map((action, index) => ( -
-
-
-
- - PR #{action.pullRequestId} - - - {action.prTitle} + // Pour les actions skipped, séparer celles avec champs préservés de celles sans changement + if (type === 'skipped') { + const withPreservedFields = typeActions.filter( + (action) => action.changes && action.changes.length > 0 + ); + const withoutChanges = typeActions.filter( + (action) => !action.changes || action.changes.length === 0 + ); + + return ( +
+

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

+ + {/* PRs avec champs préservés */} + {withPreservedFields.length > 0 && ( +
+
+
+ + Avec champs préservés ({withPreservedFields.length}) +
-
- - {getActionLabel(action.type)} - -
+
+ {withPreservedFields.map((action, index) => ( +
+
+
+
+ + PR #{action.pullRequestId} + + + {action.prTitle} + +
+
+ + {getActionLabel(action.type)} + +
- {action.reason && ( -
- 💡 {action.reason} + {action.reason && ( +
+ 💡 {action.reason} +
+ )} + + {action.changes && action.changes.length > 0 && ( +
+
+ Champs préservés: +
+ {action.changes.map((change, changeIndex) => ( +
+ {change} +
+ ))} +
+ )} +
+ ))} +
)} - {action.changes && action.changes.length > 0 && ( -
-
- Modifications: -
- {action.changes.map((change, changeIndex) => ( -
- {change} + {/* PRs sans changement */} + {withoutChanges.length > 0 && ( +
+ {withPreservedFields.length > 0 && ( +
+
+ + Aucun changement ({withoutChanges.length}) + +
- ))} + )} +
+ {withoutChanges.map((action, index) => ( +
+
+
+
+ + PR #{action.pullRequestId} + + + {action.prTitle} + +
+
+ + {getActionLabel(action.type)} + +
+ + {action.reason && ( +
+ 💡 {action.reason} +
+ )} +
+ ))} +
)}
- ))} -
-
- ))} + ); + } + + // Pour les autres types, affichage normal + return ( +
+

+ {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} +
+ ))} +
+ )} +
+ ))} +
+
+ ); + })}
); } diff --git a/src/components/tfs/TfsSync.tsx b/src/components/tfs/TfsSync.tsx index 6c2b79d..a354773 100644 --- a/src/components/tfs/TfsSync.tsx +++ b/src/components/tfs/TfsSync.tsx @@ -383,70 +383,215 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) { {} as Record ); + // Ordre d'affichage : créées, mises à jour, supprimées, ignorées (toujours en dernier) + const displayOrder: TfsSyncAction['type'][] = [ + 'created', + 'updated', + 'deleted', + 'skipped', + ]; + return (
- {Object.entries(groupedActions).map(([type, typeActions]) => ( -
-

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

+ {displayOrder + .filter((type) => groupedActions[type]?.length > 0) + .map((type) => { + const typeActions = groupedActions[type] as TfsSyncAction[]; -
- {(typeActions as TfsSyncAction[]).map( - (action: TfsSyncAction, index: number) => ( -
action.changes && action.changes.length > 0 + ); + const withoutChanges = typeActions.filter( + (action) => !action.changes || action.changes.length === 0 + ); + + return ( +
+

-
-
-
- - PR #{action.pullRequestId} - - - {action.prTitle} - -
+ {getActionIcon(type as TfsSyncAction['type'])} + {getActionLabel(type as TfsSyncAction['type'])} ( + {typeActions.length}) +

+ + {/* PRs avec champs préservés */} + {withPreservedFields.length > 0 && ( +
+
+
+ + Avec champs préservés ({withPreservedFields.length}) + +
+
+
+ {withPreservedFields.map((action, index) => ( +
+
+
+
+ + PR #{action.pullRequestId} + + + {action.prTitle} + +
+
+ + {getActionLabel(action.type)} + +
+ + {action.reason && ( +
+ 💡 {action.reason} +
+ )} + + {action.changes && action.changes.length > 0 && ( +
+
+ Champs préservés: +
+ {action.changes.map((change, changeIndex) => ( +
+ {change} +
+ ))} +
+ )} +
+ ))}
- - {getActionLabel(action.type)} -
+ )} - {action.reason && ( -
- 💡 {action.reason} -
- )} - - {action.changes && action.changes.length > 0 && ( -
-
- Modifications: + {/* PRs sans changement */} + {withoutChanges.length > 0 && ( +
+ {withPreservedFields.length > 0 && ( +
+
+ + Aucun changement ({withoutChanges.length}) + +
- {action.changes.map( - (change: string, changeIndex: number) => ( + )} +
+ {withoutChanges.map((action, index) => ( +
+
+
+
+ + PR #{action.pullRequestId} + + + {action.prTitle} + +
+
+ + {getActionLabel(action.type)} + +
+ + {action.reason && ( +
+ 💡 {action.reason} +
+ )} +
+ ))} +
+
+ )} +
+ ); + } + + // Pour les autres types, affichage normal + return ( +
+

+ {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}
- ) - )} -
- )} -
- ) - )} -
-
- ))} + ))} +
+ )} +
+ ))} +
+
+ ); + })}
); } diff --git a/src/services/integrations/jira/__tests__/sync.test.ts b/src/services/integrations/jira/__tests__/sync.test.ts index 342f280..67f92ab 100644 --- a/src/services/integrations/jira/__tests__/sync.test.ts +++ b/src/services/integrations/jira/__tests__/sync.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect } from 'vitest'; import { buildSyncUpdateData } from '@/services/task-management/readonly-fields'; +import { detectSyncChanges } from '@/services/integrations/jira/jira'; describe('Synchronisation Jira - Logique de préservation des champs', () => { describe('buildSyncUpdateData pour Jira', () => { @@ -168,4 +169,263 @@ describe('Synchronisation Jira - Logique de préservation des champs', () => { expect(result.priority).toBe('medium'); // Préservé car modifié localement }); }); + + describe('detectSyncChanges - Détection des changements et préservations', () => { + it('devrait détecter une priorité préservée localement', () => { + const existingTask = { + title: 'Tâche test', + description: 'Description locale', + status: 'todo', + priority: 'high', // Modifié localement + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const taskData = { + title: 'Tâche test', + description: 'Description locale', + status: 'todo', + priority: 'medium', // Valeur Jira différente + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const finalData = buildSyncUpdateData('jira', existingTask, taskData); + + const { realChanges, preservedFields } = detectSyncChanges( + existingTask, + taskData, + finalData + ); + + expect(realChanges).toHaveLength(0); + expect(preservedFields).toHaveLength(1); + expect(preservedFields[0]).toBe( + 'Priorité: préservée localement (high) vs Jira (medium)' + ); + }); + + it('devrait détecter un titre préservé localement', () => { + const existingTask = { + title: 'Titre modifié localement', + description: 'Description', + status: 'todo', + priority: 'medium', + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const taskData = { + title: 'Titre original Jira', + description: 'Description', + status: 'todo', + priority: 'medium', + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const finalData = buildSyncUpdateData('jira', existingTask, taskData); + + const { realChanges, preservedFields } = detectSyncChanges( + existingTask, + taskData, + finalData + ); + + expect(realChanges).toHaveLength(0); + expect(preservedFields).toHaveLength(1); + expect(preservedFields[0]).toContain('Titre: préservé localement'); + expect(preservedFields[0]).toContain('vs Jira'); + }); + + it('devrait détecter plusieurs champs préservés', () => { + const existingTask = { + title: 'Titre modifié', + description: 'Description', + status: 'todo', + priority: 'high', // Modifié localement + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const taskData = { + title: 'Titre original', // Différent + description: 'Description', + status: 'todo', + priority: 'medium', // Différent + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const finalData = buildSyncUpdateData('jira', existingTask, taskData); + + const { realChanges, preservedFields } = detectSyncChanges( + existingTask, + taskData, + finalData + ); + + expect(realChanges).toHaveLength(0); + expect(preservedFields.length).toBeGreaterThanOrEqual(2); + expect(preservedFields.some((f) => f.includes('Titre'))).toBe(true); + expect(preservedFields.some((f) => f.includes('Priorité'))).toBe(true); + }); + + it('devrait détecter un changement réel de statut', () => { + const existingTask = { + title: 'Tâche', + description: 'Description', + status: 'todo', + priority: 'medium', + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const taskData = { + title: 'Tâche', + description: 'Description', + status: 'in_progress', // Changé dans Jira + priority: 'medium', + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const finalData = buildSyncUpdateData('jira', existingTask, taskData); + + const { realChanges, preservedFields } = detectSyncChanges( + existingTask, + taskData, + finalData + ); + + expect(realChanges.length).toBeGreaterThan(0); + expect(realChanges.some((c) => c.includes('Statut'))).toBe(true); + expect(preservedFields).toHaveLength(0); + }); + + it('devrait détecter changement réel + préservation simultanés', () => { + const existingTask = { + title: 'Titre modifié localement', + description: 'Description locale', + status: 'todo', + priority: 'high', // Modifié localement + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const taskData = { + title: 'Titre original', // Différent + description: 'Description Jira', // Différent + status: 'in_progress', // Changé dans Jira + priority: 'medium', // Différent + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const finalData = buildSyncUpdateData('jira', existingTask, taskData); + + const { realChanges, preservedFields } = detectSyncChanges( + existingTask, + taskData, + finalData + ); + + // Description et statut devraient être dans realChanges (écrasés) + expect(realChanges.length).toBeGreaterThan(0); + // Titre et priorité devraient être préservés + expect(preservedFields.length).toBeGreaterThan(0); + expect(preservedFields.some((f) => f.includes('Titre'))).toBe(true); + expect(preservedFields.some((f) => f.includes('Priorité'))).toBe(true); + }); + + it('devrait détecter aucun changement quand tout est identique', () => { + const existingTask = { + title: 'Tâche', + description: 'Description', + status: 'todo', + priority: 'medium', + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const taskData = { + title: 'Tâche', + description: 'Description', + status: 'todo', + priority: 'medium', + dueDate: null, + jiraProject: 'TEST', + jiraType: 'Task', + assignee: 'User', + }; + + const finalData = buildSyncUpdateData('jira', existingTask, taskData); + + const { realChanges, preservedFields } = detectSyncChanges( + existingTask, + taskData, + finalData + ); + + expect(realChanges).toHaveLength(0); + expect(preservedFields).toHaveLength(0); + }); + + it('devrait détecter un changement de projet Jira', () => { + const existingTask = { + title: 'Tâche', + description: 'Description', + status: 'todo', + priority: 'medium', + dueDate: null, + jiraProject: 'OLD', + jiraType: 'Task', + assignee: 'User', + }; + + const taskData = { + title: 'Tâche', + description: 'Description', + status: 'todo', + priority: 'medium', + dueDate: null, + jiraProject: 'NEW', // Changé + jiraType: 'Task', + assignee: 'User', + }; + + const finalData = buildSyncUpdateData('jira', existingTask, taskData); + + const { realChanges, preservedFields } = detectSyncChanges( + existingTask, + taskData, + finalData + ); + + expect(realChanges.length).toBeGreaterThan(0); + expect(realChanges.some((c) => c.includes('Projet'))).toBe(true); + }); + }); }); diff --git a/src/services/integrations/jira/jira.ts b/src/services/integrations/jira/jira.ts index 8224092..6a9a34a 100644 --- a/src/services/integrations/jira/jira.ts +++ b/src/services/integrations/jira/jira.ts @@ -46,6 +46,8 @@ export interface SyncResult { skipped: number; deleted: number; }; + // Actions originales Jira avec tous les détails (changes, etc.) + jiraActions?: JiraSyncAction[]; } export interface JiraSyncResult { @@ -60,6 +62,126 @@ export interface JiraSyncResult { actions: JiraSyncAction[]; // Détail des actions effectuées } +/** + * Interface pour les données de tâche existante + */ +interface ExistingTaskData { + title: string; + description: string | null; + status: string; + priority: string; + dueDate: Date | null; + jiraProject: string | null; + jiraType: string | null; + assignee: string | null; +} + +/** + * Détecte les changements réels et les préservations lors de la synchronisation + * @param existingTask - Données existantes de la tâche + * @param taskData - Données venant de Jira (avec champs Jira spécifiques) + * @param finalData - Données finales après application de la logique de préservation (sans champs Jira) + * @returns Objet contenant les changements réels et les champs préservés + */ +export function detectSyncChanges( + existingTask: ExistingTaskData, + taskData: { + title: string; + description: string | null; + status: string; + priority: string; + dueDate: Date | null; + jiraProject: string; + jiraType: string; + assignee: string | null; + }, + finalData: { + title: string; + description: string | null; + status: string; + priority: string; + dueDate: Date | null; + } +): { realChanges: string[]; preservedFields: string[] } { + const realChanges: string[] = []; + const preservedFields: string[] = []; + + // Détecter les changements réels et les préservations + // Pour chaque champ, comparer existingTask avec taskData pour voir les différences + if (existingTask.title !== taskData.title) { + if (existingTask.title !== finalData.title) { + // Changement réel appliqué + realChanges.push(`Titre: ${existingTask.title} → ${finalData.title}`); + } else { + // Valeur préservée (finalData === existingTask mais différent de taskData) + preservedFields.push( + `Titre: préservé localement ("${existingTask.title}") vs Jira ("${taskData.title}")` + ); + } + } + + if (existingTask.description !== taskData.description) { + if (existingTask.description !== finalData.description) { + realChanges.push(`Description modifiée`); + } + } + + if (existingTask.status !== taskData.status) { + if (existingTask.status !== finalData.status) { + realChanges.push(`Statut: ${existingTask.status} → ${finalData.status}`); + } else { + // Valeur préservée (finalData === existingTask mais différent de taskData) + preservedFields.push( + `Statut: préservé localement (${existingTask.status}) vs Jira (${taskData.status})` + ); + } + } + + if (existingTask.priority !== taskData.priority) { + if (existingTask.priority !== finalData.priority) { + realChanges.push( + `Priorité: ${existingTask.priority} → ${finalData.priority}` + ); + } else { + // Valeur préservée (finalData === existingTask mais différent de taskData) + preservedFields.push( + `Priorité: préservée localement (${existingTask.priority}) vs Jira (${taskData.priority})` + ); + } + } + + if ( + (existingTask.dueDate?.getTime() || null) !== + (finalData.dueDate?.getTime() || null) + ) { + const oldDate = existingTask.dueDate + ? formatDateForDisplay(existingTask.dueDate) + : 'Aucune'; + const newDate = finalData.dueDate + ? formatDateForDisplay(finalData.dueDate) + : 'Aucune'; + realChanges.push(`Échéance: ${oldDate} → ${newDate}`); + } + + if (existingTask.jiraProject !== taskData.jiraProject) { + realChanges.push( + `Projet: ${existingTask.jiraProject} → ${taskData.jiraProject}` + ); + } + + if (existingTask.jiraType !== taskData.jiraType) { + realChanges.push(`Type: ${existingTask.jiraType} → ${taskData.jiraType}`); + } + + if (existingTask.assignee !== taskData.assignee) { + realChanges.push( + `Assigné: ${existingTask.assignee} → ${taskData.assignee}` + ); + } + + return { realChanges, preservedFields }; +} + export class JiraService { readonly config: JiraConfig; @@ -437,6 +559,8 @@ export class JiraService { // Déterminer le succès et enregistrer le log result.success = result.errors.length === 0; + // Ajouter les actions Jira originales pour préserver les détails (changes, etc.) + result.jiraActions = jiraActions; await this.logSync(result); console.log('✅ Synchronisation Jira terminée:', result); @@ -531,65 +655,15 @@ export class JiraService { // Utiliser la logique centralisée pour déterminer quels champs préserver const finalData = buildSyncUpdateData('jira', existingTask, taskData); - // Détecter les changements et créer la liste des modifications - const changes: string[] = []; + // Détecter les changements réels et préservations + const { realChanges, preservedFields } = detectSyncChanges( + existingTask, + taskData, + finalData + ); - // Détecter les changements en comparant les valeurs finales avec les existantes - if (existingTask.title !== finalData.title) { - changes.push(`Titre: ${existingTask.title} → ${finalData.title}`); - } else if (existingTask.title !== taskData.title) { - // Valeur préservée (finalData === existingTask mais différent de taskData) - changes.push(`Titre: préservé localement ("${existingTask.title}")`); - } - - if (existingTask.description !== finalData.description) { - changes.push(`Description modifiée`); - } - - if (existingTask.status !== finalData.status) { - changes.push(`Statut: ${existingTask.status} → ${finalData.status}`); - } else if (existingTask.status !== taskData.status) { - // Valeur préservée (finalData === existingTask mais différent de taskData) - changes.push(`Statut: préservé localement (${existingTask.status})`); - } - - if (existingTask.priority !== finalData.priority) { - changes.push( - `Priorité: ${existingTask.priority} → ${finalData.priority}` - ); - } else if (existingTask.priority !== taskData.priority) { - // Valeur préservée (finalData === existingTask mais différent de taskData) - changes.push( - `Priorité: préservée localement (${existingTask.priority})` - ); - } - if ( - (existingTask.dueDate?.getTime() || null) !== - (finalData.dueDate?.getTime() || null) - ) { - const oldDate = existingTask.dueDate - ? formatDateForDisplay(existingTask.dueDate) - : 'Aucune'; - const newDate = finalData.dueDate - ? formatDateForDisplay(finalData.dueDate) - : 'Aucune'; - changes.push(`Échéance: ${oldDate} → ${newDate}`); - } - if (existingTask.jiraProject !== taskData.jiraProject) { - changes.push( - `Projet: ${existingTask.jiraProject} → ${taskData.jiraProject}` - ); - } - if (existingTask.jiraType !== taskData.jiraType) { - changes.push(`Type: ${existingTask.jiraType} → ${taskData.jiraType}`); - } - if (existingTask.assignee !== taskData.assignee) { - changes.push( - `Assigné: ${existingTask.assignee} → ${taskData.assignee}` - ); - } - - if (changes.length === 0) { + // Si tous les champs sont préservés et aucun changement réel, skip + if (realChanges.length === 0) { console.log( `⏭️ Aucun changement pour ${jiraTask.key}, skip mise à jour` ); @@ -601,7 +675,8 @@ export class JiraService { type: 'skipped', taskKey: jiraTask.key, taskTitle: jiraTask.summary, - reason: 'Aucun changement détecté', + reason: 'Tous les champs préservés localement', + changes: preservedFields.length > 0 ? preservedFields : undefined, }; } @@ -642,14 +717,17 @@ export class JiraService { // S'assurer que le tag Jira est assigné (pour les anciennes tâches) await this.assignJiraTag(existingTask.id, userId); + // Construire la liste complète des changements (réels + préservations pour info) + const allChanges = [...realChanges, ...preservedFields]; + console.log( - `🔄 Tâche mise à jour (titre/priorité préservés): ${jiraTask.key} (${changes.length} changement${changes.length > 1 ? 's' : ''})` + `🔄 Tâche mise à jour: ${jiraTask.key} (${realChanges.length} changement${realChanges.length > 1 ? 's' : ''} réel${realChanges.length > 1 ? 's' : ''}${preservedFields.length > 0 ? `, ${preservedFields.length} préservé${preservedFields.length > 1 ? 's' : ''}` : ''})` ); return { type: 'updated', taskKey: jiraTask.key, taskTitle: jiraTask.summary, - changes, + changes: allChanges, }; } }