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
|
||||
- [ ] **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)
|
||||
|
||||
---
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-3 text-sm">
|
||||
@@ -141,6 +141,25 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
|
||||
@@ -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<string>): Promise<JiraSyncAction[]> {
|
||||
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<string>();
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<number>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user