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 { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { jiraClient } from '@/clients/jira-client';
|
||||
import { JiraSyncResult } from '@/services/jira';
|
||||
import { JiraSyncResult, JiraSyncAction } from '@/services/jira';
|
||||
|
||||
interface JiraSyncProps {
|
||||
onSyncComplete?: () => void;
|
||||
@@ -18,6 +19,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [lastSyncResult, setLastSyncResult] = useState<JiraSyncResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
const testConnection = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -67,20 +69,25 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
||||
const getSyncStatus = () => {
|
||||
if (!lastSyncResult) return null;
|
||||
|
||||
const { success, tasksCreated, tasksUpdated, tasksSkipped, errors } = lastSyncResult;
|
||||
const { success, tasksFound, tasksCreated, tasksUpdated, tasksSkipped, tasksDeleted = 0, errors, actions = [] } = lastSyncResult;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={success ? "success" : "danger"} size="sm">
|
||||
{success ? "✓ Succès" : "⚠ Erreurs"}
|
||||
</Badge>
|
||||
<span className="text-[var(--muted-foreground)]">
|
||||
{new Date().toLocaleTimeString()}
|
||||
</span>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={success ? "success" : "danger"} size="sm">
|
||||
{success ? "✓ Succès" : "⚠ Erreurs"}
|
||||
</Badge>
|
||||
<span className="text-[var(--muted-foreground)] text-xs">
|
||||
{new Date().toLocaleTimeString()}
|
||||
</span>
|
||||
</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="font-mono font-bold text-emerald-400">{tasksCreated}</div>
|
||||
<div className="text-[var(--muted-foreground)]">Créées</div>
|
||||
@@ -93,14 +100,44 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
||||
<div className="font-mono font-bold text-orange-400">{tasksSkipped}</div>
|
||||
<div className="text-[var(--muted-foreground)]">Ignorées</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>
|
||||
|
||||
{errors.length > 0 && (
|
||||
<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>
|
||||
{errors.map((err, i) => (
|
||||
<div key={i} className="text-[var(--destructive)] font-mono">{err}</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) => (
|
||||
<div key={i} className="text-[var(--destructive)] font-mono text-xs">{err}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -174,8 +211,131 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
|
||||
<div>• Synchronisation unidirectionnelle (Jira → TowerControl)</div>
|
||||
<div>• Les modifications locales sont préservées</div>
|
||||
<div>• Seuls les tickets assignés sont synchronisés</div>
|
||||
<div>• Les tickets réassignés sont automatiquement supprimés</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user