feat: enhance JiraSync with detailed sync actions
- Added `JiraSyncAction` interface to track individual task actions (created, updated, skipped, deleted) during synchronization. - Updated `JiraSyncResult` to include actions for better visibility of sync outcomes. - Implemented a modal to display detailed sync results, improving user feedback on synchronization processes. - Enhanced task deletion logic to provide reasons and changes for each action, ensuring clarity in task management.
This commit is contained in:
@@ -4,8 +4,9 @@ import { useState } from 'react';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { jiraClient } from '@/clients/jira-client';
|
import { jiraClient } from '@/clients/jira-client';
|
||||||
import { JiraSyncResult } from '@/services/jira';
|
import { JiraSyncResult, JiraSyncAction } from '@/services/jira';
|
||||||
|
|
||||||
interface JiraSyncProps {
|
interface JiraSyncProps {
|
||||||
onSyncComplete?: () => void;
|
onSyncComplete?: () => void;
|
||||||
@@ -18,6 +19,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
|||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [lastSyncResult, setLastSyncResult] = useState<JiraSyncResult | null>(null);
|
const [lastSyncResult, setLastSyncResult] = useState<JiraSyncResult | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
|
||||||
const testConnection = async () => {
|
const testConnection = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -67,20 +69,25 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
|||||||
const getSyncStatus = () => {
|
const getSyncStatus = () => {
|
||||||
if (!lastSyncResult) return null;
|
if (!lastSyncResult) return null;
|
||||||
|
|
||||||
const { success, tasksCreated, tasksUpdated, tasksSkipped, errors } = lastSyncResult;
|
const { success, tasksFound, tasksCreated, tasksUpdated, tasksSkipped, tasksDeleted = 0, errors, actions = [] } = lastSyncResult;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant={success ? "success" : "danger"} size="sm">
|
<Badge variant={success ? "success" : "danger"} size="sm">
|
||||||
{success ? "✓ Succès" : "⚠ Erreurs"}
|
{success ? "✓ Succès" : "⚠ Erreurs"}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-[var(--muted-foreground)]">
|
<span className="text-[var(--muted-foreground)] text-xs">
|
||||||
{new Date().toLocaleTimeString()}
|
{new Date().toLocaleTimeString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
{tasksFound} trouvé{tasksFound > 1 ? 's' : ''} dans Jira
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div className="text-center p-2 bg-[var(--card)] rounded">
|
<div className="text-center p-2 bg-[var(--card)] rounded">
|
||||||
<div className="font-mono font-bold text-emerald-400">{tasksCreated}</div>
|
<div className="font-mono font-bold text-emerald-400">{tasksCreated}</div>
|
||||||
<div className="text-[var(--muted-foreground)]">Créées</div>
|
<div className="text-[var(--muted-foreground)]">Créées</div>
|
||||||
@@ -93,15 +100,45 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
|||||||
<div className="font-mono font-bold text-orange-400">{tasksSkipped}</div>
|
<div className="font-mono font-bold text-orange-400">{tasksSkipped}</div>
|
||||||
<div className="text-[var(--muted-foreground)]">Ignorées</div>
|
<div className="text-[var(--muted-foreground)]">Ignorées</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-center p-2 bg-[var(--card)] rounded">
|
||||||
|
<div className="font-mono font-bold text-red-400">{tasksDeleted}</div>
|
||||||
|
<div className="text-[var(--muted-foreground)]">Supprimées</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Résumé textuel avec bouton détails */}
|
||||||
|
<div className="p-2 bg-[var(--muted)]/5 rounded text-xs">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="font-medium text-[var(--muted-foreground)]">Résumé:</div>
|
||||||
|
{actions.length > 0 && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowDetails(true)}
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs px-2 py-1 h-auto"
|
||||||
|
>
|
||||||
|
Voir détails ({actions.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-[var(--muted-foreground)]">
|
||||||
|
{tasksCreated > 0 && `${tasksCreated} nouvelle${tasksCreated > 1 ? 's' : ''} • `}
|
||||||
|
{tasksUpdated > 0 && `${tasksUpdated} mise${tasksUpdated > 1 ? 's' : ''} à jour • `}
|
||||||
|
{tasksDeleted > 0 && `${tasksDeleted} supprimée${tasksDeleted > 1 ? 's' : ''} (réassignées) • `}
|
||||||
|
{tasksSkipped > 0 && `${tasksSkipped} ignorée${tasksSkipped > 1 ? 's' : ''} • `}
|
||||||
|
{(tasksCreated + tasksUpdated + tasksDeleted + tasksSkipped) === 0 && 'Aucune modification'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<div className="p-2 bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded text-xs">
|
<div className="p-2 bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded text-xs">
|
||||||
<div className="font-semibold text-[var(--destructive)] mb-1">Erreurs:</div>
|
<div className="font-semibold text-[var(--destructive)] mb-1">Erreurs ({errors.length}):</div>
|
||||||
|
<div className="space-y-1 max-h-20 overflow-y-auto">
|
||||||
{errors.map((err, i) => (
|
{errors.map((err, i) => (
|
||||||
<div key={i} className="text-[var(--destructive)] font-mono">{err}</div>
|
<div key={i} className="text-[var(--destructive)] font-mono text-xs">{err}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -174,8 +211,131 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
|||||||
<div>• Synchronisation unidirectionnelle (Jira → TowerControl)</div>
|
<div>• Synchronisation unidirectionnelle (Jira → TowerControl)</div>
|
||||||
<div>• Les modifications locales sont préservées</div>
|
<div>• Les modifications locales sont préservées</div>
|
||||||
<div>• Seuls les tickets assignés sont synchronisés</div>
|
<div>• Seuls les tickets assignés sont synchronisés</div>
|
||||||
|
<div>• Les tickets réassignés sont automatiquement supprimés</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Modal détails de synchronisation */}
|
||||||
|
{lastSyncResult && (
|
||||||
|
<Modal
|
||||||
|
isOpen={showDetails}
|
||||||
|
onClose={() => setShowDetails(false)}
|
||||||
|
title="📋 DÉTAILS DE SYNCHRONISATION"
|
||||||
|
size="xl"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-[var(--muted-foreground)]">
|
||||||
|
{(lastSyncResult.actions || []).length} action{(lastSyncResult.actions || []).length > 1 ? 's' : ''} effectuée{(lastSyncResult.actions || []).length > 1 ? 's' : ''}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="max-h-[60vh] overflow-y-auto">
|
||||||
|
{(lastSyncResult.actions || []).length > 0 ? (
|
||||||
|
<SyncActionsList actions={lastSyncResult.actions || []} />
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-[var(--muted-foreground)]">
|
||||||
|
<div className="text-2xl mb-2">📝</div>
|
||||||
|
<div>Aucun détail disponible pour cette synchronisation</div>
|
||||||
|
<div className="text-sm mt-1">Les détails sont disponibles pour les nouvelles synchronisations</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Composant pour afficher la liste des actions
|
||||||
|
function SyncActionsList({ actions }: { actions: JiraSyncAction[] }) {
|
||||||
|
const getActionIcon = (type: JiraSyncAction['type']) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'created': return '➕';
|
||||||
|
case 'updated': return '🔄';
|
||||||
|
case 'skipped': return '⏭️';
|
||||||
|
case 'deleted': return '🗑️';
|
||||||
|
default: return '❓';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionColor = (type: JiraSyncAction['type']) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'created': return 'text-emerald-400';
|
||||||
|
case 'updated': return 'text-blue-400';
|
||||||
|
case 'skipped': return 'text-orange-400';
|
||||||
|
case 'deleted': return 'text-red-400';
|
||||||
|
default: return 'text-gray-400';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionLabel = (type: JiraSyncAction['type']) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'created': return 'Créée';
|
||||||
|
case 'updated': return 'Mise à jour';
|
||||||
|
case 'skipped': return 'Ignorée';
|
||||||
|
case 'deleted': return 'Supprimée';
|
||||||
|
default: return 'Inconnue';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grouper les actions par type
|
||||||
|
const groupedActions = actions.reduce((acc, action) => {
|
||||||
|
if (!acc[action.type]) acc[action.type] = [];
|
||||||
|
acc[action.type].push(action);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, JiraSyncAction[]>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.entries(groupedActions).map(([type, typeActions]) => (
|
||||||
|
<div key={type} className="space-y-3">
|
||||||
|
<h4 className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as JiraSyncAction['type'])}`}>
|
||||||
|
{getActionIcon(type as JiraSyncAction['type'])}
|
||||||
|
{getActionLabel(type as JiraSyncAction['type'])} ({typeActions.length})
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{typeActions.map((action, index) => (
|
||||||
|
<div key={index} className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
|
||||||
|
{action.taskKey}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
||||||
|
{action.taskTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" size="sm" className="shrink-0">
|
||||||
|
{getActionLabel(action.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{action.reason && (
|
||||||
|
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
|
||||||
|
💡 {action.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{action.changes && action.changes.length > 0 && (
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
<div className="text-xs font-medium text-[var(--muted-foreground)]">
|
||||||
|
Modifications:
|
||||||
|
</div>
|
||||||
|
{action.changes.map((change, changeIndex) => (
|
||||||
|
<div key={changeIndex} className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-blue-400/30">
|
||||||
|
{change}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
126
services/jira.ts
126
services/jira.ts
@@ -12,6 +12,14 @@ export interface JiraConfig {
|
|||||||
apiToken: string;
|
apiToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JiraSyncAction {
|
||||||
|
type: 'created' | 'updated' | 'skipped' | 'deleted';
|
||||||
|
taskKey: string;
|
||||||
|
taskTitle: string;
|
||||||
|
reason?: string; // Raison du skip ou de la suppression
|
||||||
|
changes?: string[]; // Liste des champs modifiés pour les updates
|
||||||
|
}
|
||||||
|
|
||||||
export interface JiraSyncResult {
|
export interface JiraSyncResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
tasksFound: number;
|
tasksFound: number;
|
||||||
@@ -20,6 +28,7 @@ export interface JiraSyncResult {
|
|||||||
tasksSkipped: number;
|
tasksSkipped: number;
|
||||||
tasksDeleted: number;
|
tasksDeleted: number;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
actions: JiraSyncAction[]; // Détail des actions effectuées
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JiraService {
|
export class JiraService {
|
||||||
@@ -159,7 +168,8 @@ export class JiraService {
|
|||||||
tasksUpdated: 0,
|
tasksUpdated: 0,
|
||||||
tasksSkipped: 0,
|
tasksSkipped: 0,
|
||||||
tasksDeleted: 0,
|
tasksDeleted: 0,
|
||||||
errors: []
|
errors: [],
|
||||||
|
actions: []
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -180,11 +190,15 @@ export class JiraService {
|
|||||||
// Synchroniser chaque ticket
|
// Synchroniser chaque ticket
|
||||||
for (const jiraTask of jiraTasks) {
|
for (const jiraTask of jiraTasks) {
|
||||||
try {
|
try {
|
||||||
const syncResult = await this.syncSingleTask(jiraTask);
|
const syncAction = await this.syncSingleTask(jiraTask);
|
||||||
|
|
||||||
if (syncResult === 'created') {
|
// Ajouter l'action au résultat
|
||||||
|
result.actions.push(syncAction);
|
||||||
|
|
||||||
|
// Compter les actions
|
||||||
|
if (syncAction.type === 'created') {
|
||||||
result.tasksCreated++;
|
result.tasksCreated++;
|
||||||
} else if (syncResult === 'updated') {
|
} else if (syncAction.type === 'updated') {
|
||||||
result.tasksUpdated++;
|
result.tasksUpdated++;
|
||||||
} else {
|
} else {
|
||||||
result.tasksSkipped++;
|
result.tasksSkipped++;
|
||||||
@@ -196,7 +210,9 @@ export class JiraService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Nettoyer les tâches Jira qui ne sont plus assignées à l'utilisateur
|
// Nettoyer les tâches Jira qui ne sont plus assignées à l'utilisateur
|
||||||
result.tasksDeleted = await this.cleanupUnassignedTasks(currentJiraIds);
|
const deletedActions = await this.cleanupUnassignedTasks(currentJiraIds);
|
||||||
|
result.tasksDeleted = deletedActions.length;
|
||||||
|
result.actions.push(...deletedActions);
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -217,7 +233,7 @@ export class JiraService {
|
|||||||
/**
|
/**
|
||||||
* Synchronise un ticket Jira unique
|
* Synchronise un ticket Jira unique
|
||||||
*/
|
*/
|
||||||
private async syncSingleTask(jiraTask: JiraTask): Promise<'created' | 'updated' | 'skipped'> {
|
private async syncSingleTask(jiraTask: JiraTask): Promise<JiraSyncAction> {
|
||||||
// Chercher la tâche existante
|
// Chercher la tâche existante
|
||||||
const existingTask = await prisma.task.findUnique({
|
const existingTask = await prisma.task.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -256,7 +272,11 @@ export class JiraService {
|
|||||||
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}`);
|
||||||
return 'created';
|
return {
|
||||||
|
type: 'created',
|
||||||
|
taskKey: jiraTask.key,
|
||||||
|
taskTitle: jiraTask.summary
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// Vérifier si mise à jour nécessaire (seulement si pas de modifs locales récentes)
|
// Vérifier si mise à jour nécessaire (seulement si pas de modifs locales récentes)
|
||||||
const jiraUpdated = new Date(jiraTask.updated);
|
const jiraUpdated = new Date(jiraTask.updated);
|
||||||
@@ -265,28 +285,56 @@ export class JiraService {
|
|||||||
// Si la tâche locale a été modifiée après la dernière update Jira, on skip
|
// Si la tâche locale a été modifiée après la dernière update Jira, on skip
|
||||||
if (localUpdated > jiraUpdated) {
|
if (localUpdated > jiraUpdated) {
|
||||||
console.log(`⏭️ Tâche ${jiraTask.key} modifiée localement, skip mise à jour`);
|
console.log(`⏭️ Tâche ${jiraTask.key} modifiée localement, skip mise à jour`);
|
||||||
return 'skipped';
|
return {
|
||||||
|
type: 'skipped',
|
||||||
|
taskKey: jiraTask.key,
|
||||||
|
taskTitle: jiraTask.summary,
|
||||||
|
reason: 'Modifiée localement après la dernière mise à jour Jira'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier s'il y a vraiment des changements
|
// Détecter les changements et créer la liste des modifications
|
||||||
const hasChanges =
|
const changes: string[] = [];
|
||||||
existingTask.title !== taskData.title ||
|
|
||||||
existingTask.description !== taskData.description ||
|
|
||||||
existingTask.status !== taskData.status ||
|
|
||||||
existingTask.priority !== taskData.priority ||
|
|
||||||
(existingTask.dueDate?.getTime() || null) !== (taskData.dueDate?.getTime() || null) ||
|
|
||||||
existingTask.jiraProject !== taskData.jiraProject ||
|
|
||||||
existingTask.jiraKey !== taskData.jiraKey ||
|
|
||||||
existingTask.jiraType !== taskData.jiraType ||
|
|
||||||
existingTask.assignee !== taskData.assignee;
|
|
||||||
|
|
||||||
if (!hasChanges) {
|
if (existingTask.title !== taskData.title) {
|
||||||
|
changes.push(`Titre: "${existingTask.title}" → "${taskData.title}"`);
|
||||||
|
}
|
||||||
|
if (existingTask.description !== taskData.description) {
|
||||||
|
changes.push(`Description modifiée`);
|
||||||
|
}
|
||||||
|
if (existingTask.status !== taskData.status) {
|
||||||
|
changes.push(`Statut: ${existingTask.status} → ${taskData.status}`);
|
||||||
|
}
|
||||||
|
if (existingTask.priority !== taskData.priority) {
|
||||||
|
changes.push(`Priorité: ${existingTask.priority} → ${taskData.priority}`);
|
||||||
|
}
|
||||||
|
if ((existingTask.dueDate?.getTime() || null) !== (taskData.dueDate?.getTime() || null)) {
|
||||||
|
const oldDate = existingTask.dueDate ? existingTask.dueDate.toLocaleDateString() : 'Aucune';
|
||||||
|
const newDate = taskData.dueDate ? taskData.dueDate.toLocaleDateString() : '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) {
|
||||||
console.log(`⏭️ Aucun changement pour ${jiraTask.key}, skip mise à jour`);
|
console.log(`⏭️ Aucun changement pour ${jiraTask.key}, skip mise à jour`);
|
||||||
|
|
||||||
// S'assurer que le tag Jira est assigné (pour les anciennes tâches) même en skip
|
// S'assurer que le tag Jira est assigné (pour les anciennes tâches) même en skip
|
||||||
await this.assignJiraTag(existingTask.id);
|
await this.assignJiraTag(existingTask.id);
|
||||||
|
|
||||||
return 'skipped';
|
return {
|
||||||
|
type: 'skipped',
|
||||||
|
taskKey: jiraTask.key,
|
||||||
|
taskTitle: jiraTask.summary,
|
||||||
|
reason: 'Aucun changement détecté'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mettre à jour seulement les champs Jira (pas les modifs locales)
|
// Mettre à jour seulement les champs Jira (pas les modifs locales)
|
||||||
@@ -309,15 +357,20 @@ export class JiraService {
|
|||||||
// 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)
|
||||||
await this.assignJiraTag(existingTask.id);
|
await this.assignJiraTag(existingTask.id);
|
||||||
|
|
||||||
console.log(`🔄 Tâche mise à jour: ${jiraTask.key}`);
|
console.log(`🔄 Tâche mise à jour: ${jiraTask.key} (${changes.length} changement${changes.length > 1 ? 's' : ''})`);
|
||||||
return 'updated';
|
return {
|
||||||
|
type: 'updated',
|
||||||
|
taskKey: jiraTask.key,
|
||||||
|
taskTitle: jiraTask.summary,
|
||||||
|
changes
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
private async cleanupUnassignedTasks(currentJiraIds: Set<string>): Promise<number> {
|
private async cleanupUnassignedTasks(currentJiraIds: Set<string>): Promise<JiraSyncAction[]> {
|
||||||
try {
|
try {
|
||||||
console.log('🧹 Début du nettoyage des tâches non assignées...');
|
console.log('🧹 Début du nettoyage des tâches non assignées...');
|
||||||
|
|
||||||
@@ -329,7 +382,8 @@ export class JiraService {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
sourceId: true,
|
sourceId: true,
|
||||||
jiraKey: true
|
jiraKey: true,
|
||||||
|
title: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -342,12 +396,12 @@ export class JiraService {
|
|||||||
|
|
||||||
if (tasksToDelete.length === 0) {
|
if (tasksToDelete.length === 0) {
|
||||||
console.log('✅ Aucune tâche à supprimer');
|
console.log('✅ Aucune tâche à supprimer');
|
||||||
return 0;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🗑️ ${tasksToDelete.length} tâche(s) à supprimer (plus assignées à l'utilisateur)`);
|
console.log(`🗑️ ${tasksToDelete.length} tâche(s) à supprimer (plus assignées à l'utilisateur)`);
|
||||||
|
|
||||||
let deletedCount = 0;
|
const deletedActions: JiraSyncAction[] = [];
|
||||||
|
|
||||||
// Supprimer les tâches une par une avec logging
|
// Supprimer les tâches une par une avec logging
|
||||||
for (const task of tasksToDelete) {
|
for (const task of tasksToDelete) {
|
||||||
@@ -356,19 +410,25 @@ export class JiraService {
|
|||||||
where: { id: task.id }
|
where: { id: task.id }
|
||||||
});
|
});
|
||||||
console.log(`🗑️ Tâche supprimée: ${task.jiraKey} (non assignée)`);
|
console.log(`🗑️ Tâche supprimée: ${task.jiraKey} (non assignée)`);
|
||||||
deletedCount++;
|
|
||||||
|
deletedActions.push({
|
||||||
|
type: 'deleted',
|
||||||
|
taskKey: task.jiraKey || 'UNKNOWN',
|
||||||
|
taskTitle: task.title,
|
||||||
|
reason: 'Plus assignée à l\'utilisateur actuel'
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Erreur suppression tâche ${task.jiraKey}:`, error);
|
console.error(`❌ Erreur suppression tâche ${task.jiraKey}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ Nettoyage terminé: ${deletedCount} tâche(s) supprimée(s)`);
|
console.log(`✅ Nettoyage terminé: ${deletedActions.length} tâche(s) supprimée(s)`);
|
||||||
return deletedCount;
|
return deletedActions;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Erreur lors du nettoyage des tâches non assignées:', error);
|
console.error('❌ Erreur lors du nettoyage des tâches non assignées:', error);
|
||||||
// Ne pas faire échouer la sync pour un problème de nettoyage
|
// Ne pas faire échouer la sync pour un problème de nettoyage
|
||||||
return 0;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,16 +550,16 @@ export class JiraService {
|
|||||||
'Code Review': 'in_progress',
|
'Code Review': 'in_progress',
|
||||||
'Code review': 'in_progress', // Variante casse
|
'Code review': 'in_progress', // Variante casse
|
||||||
'Testing': 'in_progress',
|
'Testing': 'in_progress',
|
||||||
'Validating': 'in_progress', // Phase de validation
|
'Product Delivery': 'in_progress', // Livré en prod
|
||||||
|
|
||||||
// Statuts "Done"
|
// Statuts "Done"
|
||||||
'Done': 'done',
|
'Done': 'done',
|
||||||
'Closed': 'done',
|
'Closed': 'done',
|
||||||
'Resolved': 'done',
|
'Resolved': 'done',
|
||||||
'Complete': 'done',
|
'Complete': 'done',
|
||||||
'Product Delivery': 'done', // Livré en prod
|
|
||||||
|
|
||||||
// Statuts bloqués
|
// Statuts bloqués
|
||||||
|
'Validating': 'blocked', // Phase de validation
|
||||||
'Blocked': 'blocked',
|
'Blocked': 'blocked',
|
||||||
'On Hold': 'blocked',
|
'On Hold': 'blocked',
|
||||||
'En attente du support': 'blocked' // Français - bloqué en attente
|
'En attente du support': 'blocked' // Français - bloqué en attente
|
||||||
|
|||||||
Reference in New Issue
Block a user