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:
Julien Froidefond
2025-10-04 11:49:41 +02:00
parent ffd3eb998a
commit b2a8c961a8
5 changed files with 156 additions and 27 deletions

View File

@@ -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(),

View File

@@ -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 é 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>
);
};

View File

@@ -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;
}
/**

View File

@@ -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