feat: jira and synchro

This commit is contained in:
Julien Froidefond
2025-09-17 13:56:42 +02:00
parent 2f104109db
commit 625e8dba4b
24 changed files with 1821 additions and 140 deletions

View File

@@ -0,0 +1,148 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { formatDistanceToNow } from 'date-fns';
import { fr } from 'date-fns/locale';
interface SyncLog {
id: string;
source: string;
status: string;
message: string | null;
tasksSync: number;
createdAt: string;
}
interface JiraLogsProps {
className?: string;
}
export function JiraLogs({ className = "" }: JiraLogsProps) {
const [logs, setLogs] = useState<SyncLog[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchLogs = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch('/api/jira/logs?limit=10');
if (!response.ok) {
throw new Error('Erreur lors de la récupération des logs');
}
const { data } = await response.json();
setLogs(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur inconnue');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchLogs();
}, []);
const getStatusBadge = (status: string) => {
switch (status) {
case 'success':
return <Badge variant="success" size="sm"> Succès</Badge>;
case 'error':
return <Badge variant="danger" size="sm"> Erreur</Badge>;
default:
return <Badge variant="outline" size="sm">{status}</Badge>;
}
};
return (
<Card className={className}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-gray-400 animate-pulse"></div>
<h3 className="font-mono text-sm font-bold text-gray-400 uppercase tracking-wider">
LOGS JIRA
</h3>
</div>
<Button
onClick={fetchLogs}
disabled={isLoading}
variant="secondary"
size="sm"
>
{isLoading ? '⟳' : '🔄'}
</Button>
</div>
</CardHeader>
<CardContent className="space-y-3">
{error && (
<div className="p-3 bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded text-sm text-[var(--destructive)] break-words overflow-hidden">
{error}
</div>
)}
{isLoading ? (
<div className="space-y-2">
{[...Array(3)].map((_, i) => (
<div key={i} className="p-3 bg-[var(--card)] rounded animate-pulse">
<div className="h-4 bg-[var(--border)] rounded w-3/4 mb-2"></div>
<div className="h-3 bg-[var(--border)] rounded w-1/2"></div>
</div>
))}
</div>
) : logs.length === 0 ? (
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm">
<div className="text-2xl mb-2">📋</div>
Aucun log de synchronisation
</div>
) : (
<div className="space-y-2 max-h-96 overflow-y-auto">
{logs.map((log) => (
<div
key={log.id}
className="p-3 bg-[var(--card)] rounded border border-[var(--border)] hover:border-[var(--border)]/70 transition-colors"
>
<div className="flex items-center justify-between mb-2">
{getStatusBadge(log.status)}
<span className="text-xs text-[var(--muted-foreground)]">
{formatDistanceToNow(new Date(log.createdAt), {
addSuffix: true,
locale: fr
})}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--foreground)] truncate flex-1 mr-2">
{log.tasksSync > 0 ? (
`${log.tasksSync} tâche${log.tasksSync > 1 ? 's' : ''} synchronisée${log.tasksSync > 1 ? 's' : ''}`
) : (
'Aucune tâche synchronisée'
)}
</span>
</div>
{log.message && (
<div className="mt-2 text-xs text-[var(--muted-foreground)] bg-[var(--background)] p-2 rounded font-mono max-h-20 overflow-y-auto break-words">
{log.message}
</div>
)}
</div>
))}
</div>
)}
{/* Info */}
<div className="text-xs text-[var(--muted-foreground)] p-2 border border-dashed border-[var(--border)] rounded">
Les logs sont conservés pour traçabilité des synchronisations
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,181 @@
'use client';
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 { jiraClient } from '@/clients/jira-client';
import { JiraSyncResult } from '@/services/jira';
interface JiraSyncProps {
onSyncComplete?: () => void;
className?: string;
}
export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
const [isConnected, setIsConnected] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const [lastSyncResult, setLastSyncResult] = useState<JiraSyncResult | null>(null);
const [error, setError] = useState<string | null>(null);
const testConnection = async () => {
setIsLoading(true);
setError(null);
try {
const status = await jiraClient.testConnection();
setIsConnected(status.connected);
if (!status.connected) {
setError(status.message);
}
} catch (err) {
setIsConnected(false);
setError(err instanceof Error ? err.message : 'Erreur de connexion');
} finally {
setIsLoading(false);
}
};
const startSync = async () => {
setIsSyncing(true);
setError(null);
try {
const result = await jiraClient.syncTasks();
setLastSyncResult(result);
if (result.success) {
onSyncComplete?.();
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur de synchronisation');
} finally {
setIsSyncing(false);
}
};
const getConnectionStatus = () => {
if (isConnected === null) return null;
return isConnected ? (
<Badge variant="success" size="sm"> Connecté</Badge>
) : (
<Badge variant="danger" size="sm"> Déconnecté</Badge>
);
};
const getSyncStatus = () => {
if (!lastSyncResult) return null;
const { success, tasksCreated, tasksUpdated, tasksSkipped, errors } = 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>
<div className="grid grid-cols-3 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>
</div>
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-blue-400">{tasksUpdated}</div>
<div className="text-[var(--muted-foreground)]">Mises à jour</div>
</div>
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-orange-400">{tasksSkipped}</div>
<div className="text-[var(--muted-foreground)]">Ignorées</div>
</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>
)}
</div>
);
};
return (
<Card className={`${className}`}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse"></div>
<h3 className="font-mono text-sm font-bold text-blue-400 uppercase tracking-wider">
JIRA SYNC
</h3>
</div>
{getConnectionStatus()}
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Test de connexion */}
<div className="flex gap-2">
<Button
onClick={testConnection}
disabled={isLoading}
variant="secondary"
size="sm"
className="flex-1"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-3 h-3 border border-[var(--muted-foreground)] border-t-transparent rounded-full animate-spin"></div>
Test...
</div>
) : (
'Tester connexion'
)}
</Button>
<Button
onClick={startSync}
disabled={isSyncing || isConnected === false}
variant="primary"
size="sm"
className="flex-1"
>
{isSyncing ? (
<div className="flex items-center gap-2">
<div className="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin"></div>
Sync...
</div>
) : (
'🔄 Synchroniser'
)}
</Button>
</div>
{/* Messages d'erreur */}
{error && (
<div className="p-3 bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded text-sm text-[var(--destructive)]">
{error}
</div>
)}
{/* Résultats de sync */}
{getSyncStatus()}
{/* Info */}
<div className="text-xs text-[var(--muted-foreground)] space-y-1">
<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>
</CardContent>
</Card>
);
}