feat: enhance Jira sync and update TODO.md
- Added handling for unknown statuses in Jira sync, logging them for better debugging and mapping to "todo" by default. - Updated sync result structure to include unknown statuses and reflected this in the UI for visibility. - Adjusted JQL to include recently resolved tasks for better status updates during sync. - Marked the integration of unknown status handling as complete in TODO.md.
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
### 🔧 Fonctionnalités et Intégrations
|
### 🔧 Fonctionnalités et Intégrations
|
||||||
- [ ] **Synchro Jira et TFS shortcuts** - Ajouter des raccourcis et bouton dans Kanban
|
- [ ] **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. <!-- Modifié cleanupUnassignedTasks (Jira) et cleanupInactivePullRequests (TFS) pour exclure les tâches done/archived de la suppression -->
|
- [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. <!-- COMPLET: 1) JQL inclut resolved >= -30d pour récupérer tâches terminées, 2) syncSingleTask met à jour status + completedAt, 3) cleanupUnassignedTasks/cleanupInactivePullRequests préservent tâches done/archived -->
|
||||||
- [ ] **Log d'activité** - Implémenter un système de log d'activité (feature potentielle)
|
- [ ] **Log d'activité** - Implémenter un système de log d'activité (feature potentielle)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export async function POST(request: Request) {
|
|||||||
tasksSkipped: syncResult.stats.skipped,
|
tasksSkipped: syncResult.stats.skipped,
|
||||||
tasksDeleted: syncResult.stats.deleted,
|
tasksDeleted: syncResult.stats.deleted,
|
||||||
errors: syncResult.errors,
|
errors: syncResult.errors,
|
||||||
|
unknownStatuses: syncResult.unknownStatuses || [], // Nouveaux statuts inconnus
|
||||||
actions: syncResult.actions.map(action => ({
|
actions: syncResult.actions.map(action => ({
|
||||||
type: action.type as 'created' | 'updated' | 'skipped' | 'deleted',
|
type: action.type as 'created' | 'updated' | 'skipped' | 'deleted',
|
||||||
taskKey: action.itemId.toString(),
|
taskKey: action.itemId.toString(),
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
|||||||
const getSyncStatus = () => {
|
const getSyncStatus = () => {
|
||||||
if (!lastSyncResult) return null;
|
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 (
|
return (
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
@@ -141,6 +141,25 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{unknownStatuses.length > 0 && (
|
||||||
|
<div className="p-2 bg-[var(--accent)]/10 border border-[var(--accent)]/20 rounded text-xs">
|
||||||
|
<div className="font-semibold text-[var(--accent)] mb-1 flex items-center gap-1">
|
||||||
|
⚠️ Statuts inconnus ({unknownStatuses.length})
|
||||||
|
</div>
|
||||||
|
<div className="text-[var(--muted-foreground)] mb-2 text-xs">
|
||||||
|
Ces statuts ont été mappés vers "todo" par défaut :
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1 max-h-20 overflow-y-auto">
|
||||||
|
{unknownStatuses.map((status, i) => (
|
||||||
|
<div key={i} className="text-[var(--accent)] font-mono text-xs flex items-center gap-1">
|
||||||
|
<span className="text-[var(--muted-foreground)]">→</span>
|
||||||
|
"{status}"
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface SyncResult {
|
|||||||
totalItems: number;
|
totalItems: number;
|
||||||
actions: SyncAction[];
|
actions: SyncAction[];
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
unknownStatuses?: string[]; // Nouveaux statuts inconnus rencontrés
|
||||||
stats: {
|
stats: {
|
||||||
created: number;
|
created: number;
|
||||||
updated: number;
|
updated: number;
|
||||||
@@ -53,6 +54,7 @@ export interface JiraSyncResult {
|
|||||||
tasksSkipped: number;
|
tasksSkipped: number;
|
||||||
tasksDeleted: number;
|
tasksDeleted: number;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
unknownStatuses?: string[]; // Statuts inconnus rencontrés
|
||||||
actions: JiraSyncAction[]; // Détail des actions effectuées
|
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é
|
* 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<JiraTask[]> {
|
async getAssignedIssues(): Promise<JiraTask[]> {
|
||||||
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);
|
return this.searchIssues(jql);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +314,9 @@ export class JiraService {
|
|||||||
try {
|
try {
|
||||||
console.log('🔄 Début de la synchronisation Jira...');
|
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
|
// S'assurer que le tag "From Jira" existe
|
||||||
await this.ensureJiraTagExists();
|
await this.ensureJiraTagExists();
|
||||||
|
|
||||||
@@ -372,6 +380,11 @@ export class JiraService {
|
|||||||
result.actions.push(standardAction);
|
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
|
// Déterminer le succès et enregistrer le log
|
||||||
result.success = result.errors.length === 0;
|
result.success = result.errors.length === 0;
|
||||||
await this.logSync(result);
|
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 = {
|
const taskData = {
|
||||||
title: jiraTask.summary,
|
title: jiraTask.summary,
|
||||||
description: jiraTask.description || null,
|
description: jiraTask.description || null,
|
||||||
@@ -418,18 +434,27 @@ export class JiraService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!existingTask) {
|
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
|
// Créer nouvelle tâche avec le tag Jira
|
||||||
const newTask = await prisma.task.create({
|
const newTask = await prisma.task.create({
|
||||||
data: {
|
data: createData
|
||||||
...taskData,
|
|
||||||
createdAt: parseDate(jiraTask.created)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assigner le tag Jira
|
// Assigner le tag Jira
|
||||||
await this.assignJiraTag(newTask.id);
|
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 {
|
return {
|
||||||
type: 'created',
|
type: 'created',
|
||||||
taskKey: jiraTask.key,
|
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)
|
// Mettre à jour les champs Jira (titre et priorité préservés si modifiés)
|
||||||
await prisma.task.update({
|
await prisma.task.update({
|
||||||
where: { id: existingTask.id },
|
where: { id: existingTask.id },
|
||||||
data: {
|
data: updateData
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// S'assurer que le tag Jira est assigné (pour les anciennes tâches)
|
// 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
|
* 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<string>): Promise<JiraSyncAction[]> {
|
private async cleanupUnassignedTasks(currentJiraIds: Set<string>): Promise<JiraSyncAction[]> {
|
||||||
try {
|
try {
|
||||||
@@ -532,16 +579,30 @@ export class JiraService {
|
|||||||
id: true,
|
id: true,
|
||||||
sourceId: true,
|
sourceId: true,
|
||||||
jiraKey: true,
|
jiraKey: true,
|
||||||
title: true
|
title: true,
|
||||||
|
status: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`📊 ${existingJiraTasks.length} tâches Jira trouvées en base`);
|
console.log(`📊 ${existingJiraTasks.length} tâches Jira trouvées en base`);
|
||||||
|
|
||||||
// Identifier les tâches à supprimer (celles qui ne sont plus dans Jira)
|
// Identifier les tâches à supprimer (celles qui ne sont plus dans Jira)
|
||||||
const tasksToDelete = existingJiraTasks.filter(task =>
|
// MAIS on exclut les tâches déjà terminées (done/archived) car elles ont été résolues
|
||||||
task.sourceId && !currentJiraIds.has(task.sourceId)
|
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) {
|
if (tasksToDelete.length === 0) {
|
||||||
console.log('✅ Aucune tâche à supprimer');
|
console.log('✅ Aucune tâche à supprimer');
|
||||||
@@ -677,6 +738,11 @@ export class JiraService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collecte des statuts inconnus pour le rapport
|
||||||
|
*/
|
||||||
|
private unknownStatuses = new Set<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mappe les statuts Jira vers les statuts internes
|
* Mappe les statuts Jira vers les statuts internes
|
||||||
*/
|
*/
|
||||||
@@ -693,6 +759,7 @@ export class JiraService {
|
|||||||
'Ouvert': 'todo', // Français
|
'Ouvert': 'todo', // Français
|
||||||
'Selected for Development': 'todo',
|
'Selected for Development': 'todo',
|
||||||
'A faire': 'todo', // Français
|
'A faire': 'todo', // Français
|
||||||
|
'Ready To Activate': 'todo', // Prêt à activer
|
||||||
|
|
||||||
// Statuts "In Progress"
|
// Statuts "In Progress"
|
||||||
'In Progress': 'in_progress',
|
'In Progress': 'in_progress',
|
||||||
@@ -705,9 +772,29 @@ export class JiraService {
|
|||||||
|
|
||||||
// Statuts "Done"
|
// Statuts "Done"
|
||||||
'Done': 'done',
|
'Done': 'done',
|
||||||
|
'DONE': 'done', // Variante majuscule
|
||||||
'Closed': 'done',
|
'Closed': 'done',
|
||||||
|
'CLOSED': 'done', // Variante majuscule
|
||||||
'Resolved': 'done',
|
'Resolved': 'done',
|
||||||
|
'RESOLVED': 'done', // Variante majuscule
|
||||||
'Complete': 'done',
|
'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
|
// Statuts bloqués
|
||||||
'Validating': 'freeze', // Phase de validation
|
'Validating': 'freeze', // Phase de validation
|
||||||
@@ -716,7 +803,15 @@ export class JiraService {
|
|||||||
'En attente du support': 'freeze' // Français - bloqué en attente
|
'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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -885,6 +885,7 @@ export class TfsService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Nettoie les tâches TFS qui ne correspondent plus aux PRs actives
|
* 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(
|
private async cleanupInactivePullRequests(
|
||||||
currentPrIds: Set<number>
|
currentPrIds: Set<number>
|
||||||
@@ -902,18 +903,31 @@ export class TfsService {
|
|||||||
sourceId: true,
|
sourceId: true,
|
||||||
tfsPullRequestId: true,
|
tfsPullRequestId: true,
|
||||||
title: true,
|
title: true,
|
||||||
|
status: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Identifier les tâches à supprimer
|
// 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 tasksToDelete = existingTfsTasks.filter((task) => {
|
||||||
const prId = task.tfsPullRequestId;
|
const prId = task.tfsPullRequestId;
|
||||||
if (!prId) {
|
if (!prId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldKeep = currentPrIds.has(prId);
|
// Si la PR est toujours active, on la garde
|
||||||
return !shouldKeep;
|
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
|
// Supprimer les tâches obsolètes
|
||||||
|
|||||||
Reference in New Issue
Block a user