diff --git a/TODO.md b/TODO.md
index 458e4b9..e7cf9be 100644
--- a/TODO.md
+++ b/TODO.md
@@ -41,7 +41,7 @@
### 🔧 Fonctionnalités et Intégrations
- [ ] **Synchro Jira et TFS shortcuts** - Ajouter des raccourcis et bouton dans Kanban
-- [x] **Intégration suppressions Jira/TFS** - Aligner la gestion des suppressions sur TFS, je veux que ce qu'on a récupéré dans la synchro, quand ca devient terminé dans Jira ou TFS, soit marqué comme terminé dans le Kanban et non supprimé du kanban.
+- [x] **Intégration suppressions Jira/TFS** - Aligner la gestion des suppressions sur TFS, je veux que ce qu'on a récupéré dans la synchro, quand ca devient terminé dans Jira ou TFS, soit marqué comme terminé dans le Kanban et non supprimé du kanban.
- [ ] **Log d'activité** - Implémenter un système de log d'activité (feature potentielle)
---
diff --git a/src/app/api/jira/sync/route.ts b/src/app/api/jira/sync/route.ts
index 79a4e2b..f6d2ee3 100644
--- a/src/app/api/jira/sync/route.ts
+++ b/src/app/api/jira/sync/route.ts
@@ -110,6 +110,7 @@ export async function POST(request: Request) {
tasksSkipped: syncResult.stats.skipped,
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(),
diff --git a/src/components/jira/JiraSync.tsx b/src/components/jira/JiraSync.tsx
index cccece0..2d2b135 100644
--- a/src/components/jira/JiraSync.tsx
+++ b/src/components/jira/JiraSync.tsx
@@ -70,7 +70,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
const getSyncStatus = () => {
if (!lastSyncResult) return null;
- const { success, tasksFound, tasksCreated, tasksUpdated, tasksSkipped, tasksDeleted = 0, errors, actions = [] } = lastSyncResult;
+ const { success, tasksFound, tasksCreated, tasksUpdated, tasksSkipped, tasksDeleted = 0, errors, unknownStatuses = [], actions = [] } = lastSyncResult;
return (
@@ -141,6 +141,25 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
)}
+
+ {unknownStatuses.length > 0 && (
+
+
+ ⚠️ Statuts inconnus ({unknownStatuses.length})
+
+
+ Ces statuts ont été mappés vers "todo" par défaut :
+
+
+ {unknownStatuses.map((status, i) => (
+
+ →
+ "{status}"
+
+ ))}
+
+
+ )}
);
};
diff --git a/src/services/integrations/jira/jira.ts b/src/services/integrations/jira/jira.ts
index 58e8fff..85bac65 100644
--- a/src/services/integrations/jira/jira.ts
+++ b/src/services/integrations/jira/jira.ts
@@ -37,6 +37,7 @@ export interface SyncResult {
totalItems: number;
actions: SyncAction[];
errors: string[];
+ unknownStatuses?: string[]; // Nouveaux statuts inconnus rencontrés
stats: {
created: number;
updated: number;
@@ -53,6 +54,7 @@ export interface JiraSyncResult {
tasksSkipped: number;
tasksDeleted: number;
errors: string[];
+ unknownStatuses?: string[]; // Statuts inconnus rencontrés
actions: JiraSyncAction[]; // Détail des actions effectuées
}
@@ -255,9 +257,12 @@ export class JiraService {
/**
* Récupère les tickets assignés à l'utilisateur connecté
+ * Inclut les tickets non résolus ET les tickets récemment résolus (30 derniers jours)
+ * pour permettre la mise Ă jour du statut vers "done" lors de la synchro
*/
async getAssignedIssues(): Promise {
- const jql = 'assignee = currentUser() AND resolution = Unresolved AND issuetype != Epic ORDER BY updated DESC';
+ // Récupérer les tickets non résolus + résolus récemment (30 jours)
+ const jql = 'assignee = currentUser() AND (resolution = Unresolved OR resolved >= -30d) AND issuetype != Epic ORDER BY updated DESC';
return this.searchIssues(jql);
}
@@ -309,6 +314,9 @@ export class JiraService {
try {
console.log('🔄 Début de la synchronisation Jira...');
+ // Réinitialiser la collecte des statuts inconnus
+ this.unknownStatuses.clear();
+
// S'assurer que le tag "From Jira" existe
await this.ensureJiraTagExists();
@@ -372,6 +380,11 @@ export class JiraService {
result.actions.push(standardAction);
}
+ // Ajouter les statuts inconnus au résultat
+ if (this.unknownStatuses.size > 0) {
+ result.unknownStatuses = Array.from(this.unknownStatuses);
+ }
+
// Déterminer le succès et enregistrer le log
result.success = result.errors.length === 0;
await this.logSync(result);
@@ -402,6 +415,9 @@ export class JiraService {
}
});
+ // Debug : Log du statut Jira pour comprendre le mapping
+ console.log(` 📝 Mapping statut Jira: "${jiraTask.status.name}" → ${this.mapJiraStatusToInternal(jiraTask.status.name)}`);
+
const taskData = {
title: jiraTask.summary,
description: jiraTask.description || null,
@@ -418,18 +434,27 @@ export class JiraService {
};
if (!existingTask) {
+ // Préparer les données de création
+ const createData: typeof taskData & { createdAt: Date; completedAt?: Date } = {
+ ...taskData,
+ createdAt: parseDate(jiraTask.created)
+ };
+
+ // Si la tâche est déjà terminée dans Jira, définir completedAt
+ if (taskData.status === 'done') {
+ createData.completedAt = new Date();
+ console.log(` ✅ Nouvelle tâche déjà terminée dans Jira (completedAt défini)`);
+ }
+
// Créer nouvelle tâche avec le tag Jira
const newTask = await prisma.task.create({
- data: {
- ...taskData,
- createdAt: parseDate(jiraTask.created)
- }
+ data: createData
});
// Assigner le tag Jira
await this.assignJiraTag(newTask.id);
- console.log(`➕ Nouvelle tâche créée: ${jiraTask.key}`);
+ console.log(`➕ Nouvelle tâche créée: ${jiraTask.key} (status: ${taskData.status})`);
return {
type: 'created',
taskKey: jiraTask.key,
@@ -486,21 +511,42 @@ export class JiraService {
};
}
+ // Préparer les données de mise à jour
+ const updateData: {
+ title: string;
+ description: string | null;
+ status: string;
+ priority: string;
+ dueDate: Date | null;
+ jiraProject: string;
+ jiraKey: string;
+ jiraType: string;
+ assignee: string | null;
+ updatedAt: Date;
+ completedAt?: Date;
+ } = {
+ title: finalTitle,
+ description: taskData.description,
+ status: taskData.status,
+ priority: finalPriority,
+ dueDate: taskData.dueDate,
+ jiraProject: taskData.jiraProject,
+ jiraKey: taskData.jiraKey,
+ jiraType: taskData.jiraType,
+ assignee: taskData.assignee,
+ updatedAt: taskData.updatedAt
+ };
+
+ // Si la tâche passe à "done" et n'a pas encore de completedAt, le définir
+ if (taskData.status === 'done' && !existingTask.completedAt) {
+ updateData.completedAt = new Date();
+ console.log(` ✅ Tâche marquée comme terminée (completedAt défini)`);
+ }
+
// Mettre à jour les champs Jira (titre et priorité préservés si modifiés)
await prisma.task.update({
where: { id: existingTask.id },
- data: {
- title: finalTitle,
- description: taskData.description,
- status: taskData.status,
- priority: finalPriority,
- dueDate: taskData.dueDate,
- jiraProject: taskData.jiraProject,
- jiraKey: taskData.jiraKey,
- jiraType: taskData.jiraType,
- assignee: taskData.assignee,
- updatedAt: taskData.updatedAt
- }
+ data: updateData
});
// S'assurer que le tag Jira est assigné (pour les anciennes tâches)
@@ -518,6 +564,7 @@ export class JiraService {
/**
* Nettoie les tâches Jira qui ne sont plus assignées à l'utilisateur
+ * Ne supprime PAS les tâches déjà terminées (done/archived) pour garder l'historique
*/
private async cleanupUnassignedTasks(currentJiraIds: Set): Promise {
try {
@@ -532,16 +579,30 @@ export class JiraService {
id: true,
sourceId: true,
jiraKey: true,
- title: true
+ title: true,
+ status: true
}
});
console.log(`📊 ${existingJiraTasks.length} tâches Jira trouvées en base`);
// Identifier les tâches à supprimer (celles qui ne sont plus dans Jira)
- const tasksToDelete = existingJiraTasks.filter(task =>
- task.sourceId && !currentJiraIds.has(task.sourceId)
- );
+ // MAIS on exclut les tâches déjà terminées (done/archived) car elles ont été résolues
+ const tasksToDelete = existingJiraTasks.filter(task => {
+ // Si la tâche est toujours dans Jira, on la garde
+ if (task.sourceId && currentJiraIds.has(task.sourceId)) {
+ return false;
+ }
+
+ // Si la tâche est déjà terminée (done/archived), on la garde en base pour l'historique
+ if (task.status === 'done' || task.status === 'archived') {
+ console.log(`✅ Tâche ${task.jiraKey} terminée - conservée en base malgré absence dans Jira`);
+ return false;
+ }
+
+ // Sinon, c'est une tâche non terminée qui n'est plus assignée → suppression
+ return task.sourceId && !currentJiraIds.has(task.sourceId);
+ });
if (tasksToDelete.length === 0) {
console.log('✅ Aucune tâche à supprimer');
@@ -677,6 +738,11 @@ export class JiraService {
};
}
+ /**
+ * Collecte des statuts inconnus pour le rapport
+ */
+ private unknownStatuses = new Set();
+
/**
* Mappe les statuts Jira vers les statuts internes
*/
@@ -693,6 +759,7 @@ export class JiraService {
'Ouvert': 'todo', // Français
'Selected for Development': 'todo',
'A faire': 'todo', // Français
+ 'Ready To Activate': 'todo', // PrĂŞt Ă activer
// Statuts "In Progress"
'In Progress': 'in_progress',
@@ -705,9 +772,29 @@ export class JiraService {
// Statuts "Done"
'Done': 'done',
+ 'DONE': 'done', // Variante majuscule
'Closed': 'done',
+ 'CLOSED': 'done', // Variante majuscule
'Resolved': 'done',
+ 'RESOLVED': 'done', // Variante majuscule
'Complete': 'done',
+ 'COMPLETE': 'done', // Variante majuscule
+ 'Finished': 'done', // Ajout
+ 'FINISHED': 'done', // Ajout majuscule
+ 'Livré': 'done', // Français
+ 'LIVRÉ': 'done', // Français majuscule
+ 'Terminé': 'done', // Français
+ 'TERMINÉ': 'done', // Français majuscule
+ 'Fait': 'done', // Français
+ 'FAIT': 'done', // Français majuscule
+ 'Clôturé': 'done', // Français - terminé/fini
+ 'CLÔTURÉ': 'done', // Français majuscule
+ 'Cloturé': 'done', // Sans accent
+ 'CLOTURÉ': 'done', // Sans accent upper
+ 'Product Learning': 'done', // Phase d'apprentissage finale
+ 'PRODUCT LEARNING': 'done', // Majuscule
+ 'In Production': 'done', // Ajout
+ 'En Production': 'done', // Français
// Statuts bloqués
'Validating': 'freeze', // Phase de validation
@@ -716,7 +803,15 @@ export class JiraService {
'En attente du support': 'freeze' // Français - bloqué en attente
};
- return statusMapping[jiraStatus] || 'todo';
+ const mapped = statusMapping[jiraStatus] || 'todo';
+
+ // Collecter et log si statut inconnu pour debug
+ if (!statusMapping[jiraStatus]) {
+ this.unknownStatuses.add(jiraStatus);
+ console.log(`⚠️ Statut Jira inconnu: "${jiraStatus}" → mappé vers "todo" par défaut`);
+ }
+
+ return mapped;
}
/**
diff --git a/src/services/integrations/tfs/tfs.ts b/src/services/integrations/tfs/tfs.ts
index 9870408..89b66bd 100644
--- a/src/services/integrations/tfs/tfs.ts
+++ b/src/services/integrations/tfs/tfs.ts
@@ -885,6 +885,7 @@ export class TfsService {
/**
* Nettoie les tâches TFS qui ne correspondent plus aux PRs actives
+ * Ne supprime PAS les tâches déjà terminées (done/archived) pour garder l'historique
*/
private async cleanupInactivePullRequests(
currentPrIds: Set
@@ -902,18 +903,31 @@ export class TfsService {
sourceId: true,
tfsPullRequestId: true,
title: true,
+ status: true,
},
});
// Identifier les tâches à supprimer
+ // MAIS on exclut les tâches déjà terminées (done/archived) car elles ont été complétées
const tasksToDelete = existingTfsTasks.filter((task) => {
const prId = task.tfsPullRequestId;
if (!prId) {
return true;
}
- const shouldKeep = currentPrIds.has(prId);
- return !shouldKeep;
+ // Si la PR est toujours active, on la garde
+ if (currentPrIds.has(prId)) {
+ return false;
+ }
+
+ // Si la tâche est déjà terminée (done/archived), on la garde en base pour l'historique
+ if (task.status === 'done' || task.status === 'archived') {
+ console.log(`✅ PR ${prId} terminée - conservée en base malgré absence dans TFS`);
+ return false;
+ }
+
+ // Sinon, c'est une tâche non terminée qui n'est plus active → suppression
+ return true;
});
// Supprimer les tâches obsolètes