feat: implement Jira auto-sync scheduler and UI configuration
- Added `jiraAutoSync` and `jiraSyncInterval` fields to user preferences for scheduler configuration. - Created `JiraScheduler` service to manage automatic synchronization with Jira based on user settings. - Updated API route to handle scheduler actions and configuration updates. - Introduced `JiraSchedulerConfig` component for user interface to control scheduler settings. - Enhanced `TODO.md` to reflect completed tasks related to Jira synchronization features.
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -1,7 +1,7 @@
|
|||||||
# TowerControl v2.0 - Gestionnaire de tâches moderne
|
# TowerControl v2.0 - Gestionnaire de tâches moderne
|
||||||
|
|
||||||
## Autre Todos #2
|
## Autre Todos #2
|
||||||
- [ ] Synchro Jira auto en background timé comme pour la synchro de sauvegarde
|
- [x] Synchro Jira auto en background timé comme pour la synchro de sauvegarde
|
||||||
- [ ] refacto des allpreferences : ca devrait eter un contexte dans le layout qui balance serverside dans le hook
|
- [ ] refacto des allpreferences : ca devrait eter un contexte dans le layout qui balance serverside dans le hook
|
||||||
- [x] backups : ne backuper que si il y a eu un changement entre le dernier backup et la base actuelle
|
- [x] backups : ne backuper que si il y a eu un changement entre le dernier backup et la base actuelle
|
||||||
- [ ] refacto des dates avec le utils qui pour l'instant n'est pas utilisé
|
- [ ] refacto des dates avec le utils qui pour l'instant n'est pas utilisé
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ model UserPreferences {
|
|||||||
// Configuration Jira (JSON)
|
// Configuration Jira (JSON)
|
||||||
jiraConfig Json?
|
jiraConfig Json?
|
||||||
|
|
||||||
|
// Configuration du scheduler Jira
|
||||||
|
jiraAutoSync Boolean @default(false)
|
||||||
|
jiraSyncInterval String @default("daily") // hourly, daily, weekly
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,55 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { createJiraService, JiraService } from '@/services/jira';
|
import { createJiraService, JiraService } from '@/services/jira';
|
||||||
import { userPreferencesService } from '@/services/user-preferences';
|
import { userPreferencesService } from '@/services/user-preferences';
|
||||||
|
import { jiraScheduler } from '@/services/jira-scheduler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route POST /api/jira/sync
|
* Route POST /api/jira/sync
|
||||||
* Synchronise les tickets Jira avec la base locale
|
* Synchronise les tickets Jira avec la base locale
|
||||||
|
* Supporte aussi les actions du scheduler
|
||||||
*/
|
*/
|
||||||
export async function POST() {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
// Essayer d'abord la config depuis la base de données
|
// Vérifier s'il y a des actions spécifiques (scheduler)
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
const { action, ...params } = body;
|
||||||
|
|
||||||
|
// Actions du scheduler
|
||||||
|
if (action) {
|
||||||
|
switch (action) {
|
||||||
|
case 'scheduler':
|
||||||
|
if (params.enabled) {
|
||||||
|
await jiraScheduler.start();
|
||||||
|
} else {
|
||||||
|
jiraScheduler.stop();
|
||||||
|
}
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: await jiraScheduler.getStatus()
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'config':
|
||||||
|
await userPreferencesService.saveJiraSchedulerConfig(
|
||||||
|
params.jiraAutoSync,
|
||||||
|
params.jiraSyncInterval
|
||||||
|
);
|
||||||
|
// Redémarrer le scheduler si la config a changé
|
||||||
|
await jiraScheduler.restart();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Configuration scheduler mise à jour',
|
||||||
|
data: await jiraScheduler.getStatus()
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: 'Action inconnue' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronisation normale (manuelle)
|
||||||
const jiraConfig = await userPreferencesService.getJiraConfig();
|
const jiraConfig = await userPreferencesService.getJiraConfig();
|
||||||
|
|
||||||
let jiraService: JiraService | null = null;
|
let jiraService: JiraService | null = null;
|
||||||
@@ -34,7 +75,7 @@ export async function POST() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔄 Début de la synchronisation Jira...');
|
console.log('🔄 Début de la synchronisation Jira manuelle...');
|
||||||
|
|
||||||
// Tester la connexion d'abord
|
// Tester la connexion d'abord
|
||||||
const connectionOk = await jiraService.testConnection();
|
const connectionOk = await jiraService.testConnection();
|
||||||
@@ -118,6 +159,9 @@ export async function GET() {
|
|||||||
projectValidation = await jiraService.validateProject(jiraConfig.projectKey);
|
projectValidation = await jiraService.validateProject(jiraConfig.projectKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Récupérer aussi le statut du scheduler
|
||||||
|
const schedulerStatus = await jiraScheduler.getStatus();
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
connected,
|
connected,
|
||||||
message: connected ? 'Connexion Jira OK' : 'Impossible de se connecter à Jira',
|
message: connected ? 'Connexion Jira OK' : 'Impossible de se connecter à Jira',
|
||||||
@@ -126,7 +170,8 @@ export async function GET() {
|
|||||||
exists: projectValidation.exists,
|
exists: projectValidation.exists,
|
||||||
name: projectValidation.name,
|
name: projectValidation.name,
|
||||||
error: projectValidation.error
|
error: projectValidation.error
|
||||||
} : null
|
} : null,
|
||||||
|
scheduler: schedulerStatus
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ export interface JiraConnectionStatus {
|
|||||||
connected: boolean;
|
connected: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
|
scheduler?: JiraSchedulerStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JiraSchedulerStatus {
|
||||||
|
isRunning: boolean;
|
||||||
|
isEnabled: boolean;
|
||||||
|
interval: 'hourly' | 'daily' | 'weekly';
|
||||||
|
nextSync: string | null;
|
||||||
|
jiraConfigured: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JiraClient extends HttpClient {
|
export class JiraClient extends HttpClient {
|
||||||
@@ -30,6 +39,29 @@ export class JiraClient extends HttpClient {
|
|||||||
const response = await this.post<{ data: JiraSyncResult }>('/sync');
|
const response = await this.post<{ data: JiraSyncResult }>('/sync');
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active/désactive le scheduler automatique
|
||||||
|
*/
|
||||||
|
async toggleScheduler(enabled: boolean): Promise<JiraSchedulerStatus> {
|
||||||
|
const response = await this.post<{ data: JiraSchedulerStatus }>('/sync', {
|
||||||
|
action: 'scheduler',
|
||||||
|
enabled
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la configuration du scheduler
|
||||||
|
*/
|
||||||
|
async updateSchedulerConfig(jiraAutoSync: boolean, jiraSyncInterval: 'hourly' | 'daily' | 'weekly'): Promise<JiraSchedulerStatus> {
|
||||||
|
const response = await this.post<{ data: JiraSchedulerStatus }>('/sync', {
|
||||||
|
action: 'config',
|
||||||
|
jiraAutoSync,
|
||||||
|
jiraSyncInterval
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance singleton
|
// Instance singleton
|
||||||
|
|||||||
208
src/components/jira/JiraSchedulerConfig.tsx
Normal file
208
src/components/jira/JiraSchedulerConfig.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { jiraClient, JiraSchedulerStatus } from '@/clients/jira-client';
|
||||||
|
|
||||||
|
interface JiraSchedulerConfigProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JiraSchedulerConfig({ className = "" }: JiraSchedulerConfigProps) {
|
||||||
|
const [schedulerStatus, setSchedulerStatus] = useState<JiraSchedulerStatus | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Charger le statut initial
|
||||||
|
useEffect(() => {
|
||||||
|
loadSchedulerStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadSchedulerStatus = async () => {
|
||||||
|
try {
|
||||||
|
const status = await jiraClient.testConnection();
|
||||||
|
if (status.scheduler) {
|
||||||
|
setSchedulerStatus(status.scheduler);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur lors du chargement du statut scheduler:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleScheduler = async () => {
|
||||||
|
if (!schedulerStatus) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Utiliser isEnabled au lieu de isRunning pour l'activation
|
||||||
|
const newStatus = await jiraClient.updateSchedulerConfig(!schedulerStatus.isEnabled, schedulerStatus.interval);
|
||||||
|
setSchedulerStatus(newStatus);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erreur lors du toggle scheduler');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInterval = async (interval: 'hourly' | 'daily' | 'weekly') => {
|
||||||
|
if (!schedulerStatus) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newStatus = await jiraClient.updateSchedulerConfig(true, interval);
|
||||||
|
setSchedulerStatus(newStatus);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = () => {
|
||||||
|
if (!schedulerStatus) return null;
|
||||||
|
|
||||||
|
if (!schedulerStatus.jiraConfigured) {
|
||||||
|
return <Badge variant="warning" size="sm">⚠️ Jira non configuré</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schedulerStatus.isEnabled) {
|
||||||
|
return <Badge variant="default" size="sm">⏸️ Désactivé</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return schedulerStatus.isRunning ? (
|
||||||
|
<Badge variant="success" size="sm">✅ Actif</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="danger" size="sm">❌ Arrêté</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextSyncText = () => {
|
||||||
|
if (!schedulerStatus?.nextSync) return 'Aucune synchronisation planifiée';
|
||||||
|
|
||||||
|
const nextSync = new Date(schedulerStatus.nextSync);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = nextSync.getTime() - now.getTime();
|
||||||
|
|
||||||
|
if (diffMs <= 0) return 'Synchronisation en cours...';
|
||||||
|
|
||||||
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
|
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
if (diffHours > 0) {
|
||||||
|
return `Dans ${diffHours}h ${diffMinutes}min`;
|
||||||
|
} else {
|
||||||
|
return `Dans ${diffMinutes}min`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIntervalText = (interval: string) => {
|
||||||
|
switch (interval) {
|
||||||
|
case 'hourly': return 'Toutes les heures';
|
||||||
|
case 'daily': return 'Quotidienne';
|
||||||
|
case 'weekly': return 'Hebdomadaire';
|
||||||
|
default: return interval;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!schedulerStatus) {
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardHeader>
|
||||||
|
<h3 className="text-lg font-semibold">⏰ Synchronisation automatique</h3>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-gray-500">Chargement...</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<h3 className="text-base sm:text-lg font-semibold flex-1 min-w-0 truncate">⏰ Synchronisation automatique</h3>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{getStatusBadge()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||||
|
<p className="text-red-700 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Statut actuel */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-600">Statut:</span>
|
||||||
|
<p className="mt-1">
|
||||||
|
{schedulerStatus.isEnabled && schedulerStatus.isRunning ? '🟢 Actif' : '🔴 Arrêté'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-600">Fréquence:</span>
|
||||||
|
<p className="mt-1">{getIntervalText(schedulerStatus.interval)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<span className="font-medium text-gray-600">Prochaine synchronisation:</span>
|
||||||
|
<p className="mt-1">{getNextSyncText()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contrôles */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{/* Toggle scheduler */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium">Synchronisation automatique</span>
|
||||||
|
<Button
|
||||||
|
variant={schedulerStatus.isEnabled ? "danger" : "primary"}
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleScheduler}
|
||||||
|
disabled={isLoading || !schedulerStatus.jiraConfigured}
|
||||||
|
>
|
||||||
|
{schedulerStatus.isEnabled ? 'Désactiver' : 'Activer'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sélecteur d'intervalle */}
|
||||||
|
{schedulerStatus.isEnabled && (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-gray-600 block mb-2">Fréquence de synchronisation</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{(['hourly', 'daily', 'weekly'] as const).map((interval) => (
|
||||||
|
<Button
|
||||||
|
key={interval}
|
||||||
|
variant={schedulerStatus.interval === interval ? "primary" : "secondary"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => updateInterval(interval)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{getIntervalText(interval)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Avertissement si Jira non configuré */}
|
||||||
|
{!schedulerStatus.jiraConfigured && (
|
||||||
|
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||||
|
<p className="text-yellow-700 text-sm">
|
||||||
|
⚠️ Configurez d'abord votre connexion Jira pour activer la synchronisation automatique.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
|||||||
import { JiraConfigForm } from '@/components/settings/JiraConfigForm';
|
import { JiraConfigForm } from '@/components/settings/JiraConfigForm';
|
||||||
import { JiraSync } from '@/components/jira/JiraSync';
|
import { JiraSync } from '@/components/jira/JiraSync';
|
||||||
import { JiraLogs } from '@/components/jira/JiraLogs';
|
import { JiraLogs } from '@/components/jira/JiraLogs';
|
||||||
|
import { JiraSchedulerConfig } from '@/components/jira/JiraSchedulerConfig';
|
||||||
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
|
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
@@ -145,6 +146,7 @@ export function IntegrationsSettingsPageClient({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<JiraSchedulerConfig />
|
||||||
<JiraSync />
|
<JiraSync />
|
||||||
<JiraLogs />
|
<JiraLogs />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ export interface UserPreferences {
|
|||||||
viewPreferences: ViewPreferences;
|
viewPreferences: ViewPreferences;
|
||||||
columnVisibility: ColumnVisibility;
|
columnVisibility: ColumnVisibility;
|
||||||
jiraConfig: JiraConfig;
|
jiraConfig: JiraConfig;
|
||||||
|
jiraAutoSync: boolean;
|
||||||
|
jiraSyncInterval: 'hourly' | 'daily' | 'weekly';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface pour les logs de synchronisation
|
// Interface pour les logs de synchronisation
|
||||||
|
|||||||
201
src/services/jira-scheduler.ts
Normal file
201
src/services/jira-scheduler.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { userPreferencesService } from './user-preferences';
|
||||||
|
import { JiraService } from './jira';
|
||||||
|
|
||||||
|
export interface JiraSchedulerConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
interval: 'hourly' | 'daily' | 'weekly';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class JiraScheduler {
|
||||||
|
private timer: NodeJS.Timeout | null = null;
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarre le planificateur de synchronisation Jira automatique
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.isRunning) {
|
||||||
|
console.log('⚠️ Jira scheduler is already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await this.getConfig();
|
||||||
|
|
||||||
|
if (!config.enabled) {
|
||||||
|
console.log('📋 Automatic Jira sync is disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que Jira est configuré
|
||||||
|
const jiraConfig = await userPreferencesService.getJiraConfig();
|
||||||
|
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) {
|
||||||
|
console.log('⚠️ Jira not configured, scheduler cannot start');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalMs = this.getIntervalMs(config.interval);
|
||||||
|
|
||||||
|
// Première synchronisation immédiate (optionnelle)
|
||||||
|
// this.performScheduledSync();
|
||||||
|
|
||||||
|
// Planifier les synchronisations suivantes
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.performScheduledSync();
|
||||||
|
}, intervalMs);
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
console.log(`✅ Jira scheduler started with ${config.interval} interval`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arrête le planificateur
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunning = false;
|
||||||
|
console.log('🛑 Jira scheduler stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redémarre le planificateur (utile lors des changements de config)
|
||||||
|
*/
|
||||||
|
async restart(): Promise<void> {
|
||||||
|
this.stop();
|
||||||
|
await this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le planificateur fonctionne
|
||||||
|
*/
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.isRunning && this.timer !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effectue une synchronisation planifiée
|
||||||
|
*/
|
||||||
|
private async performScheduledSync(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('🔄 Starting scheduled Jira sync...');
|
||||||
|
|
||||||
|
// Récupérer la config Jira
|
||||||
|
const jiraConfig = await userPreferencesService.getJiraConfig();
|
||||||
|
|
||||||
|
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) {
|
||||||
|
console.log('⚠️ Jira config incomplete, skipping scheduled sync');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le service Jira
|
||||||
|
const jiraService = new JiraService({
|
||||||
|
baseUrl: jiraConfig.baseUrl,
|
||||||
|
email: jiraConfig.email,
|
||||||
|
apiToken: jiraConfig.apiToken,
|
||||||
|
projectKey: jiraConfig.projectKey,
|
||||||
|
ignoredProjects: jiraConfig.ignoredProjects || []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tester la connexion d'abord
|
||||||
|
const connectionOk = await jiraService.testConnection();
|
||||||
|
if (!connectionOk) {
|
||||||
|
console.error('❌ Scheduled Jira sync failed: connection error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effectuer la synchronisation
|
||||||
|
const result = await jiraService.syncTasks();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ Scheduled Jira sync completed: ${result.tasksCreated} created, ${result.tasksUpdated} updated, ${result.tasksSkipped} skipped`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Scheduled Jira sync failed: ${result.errors.join(', ')}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Scheduled Jira sync error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit l'intervalle en millisecondes
|
||||||
|
*/
|
||||||
|
private getIntervalMs(interval: JiraSchedulerConfig['interval']): number {
|
||||||
|
const intervals = {
|
||||||
|
hourly: 60 * 60 * 1000, // 1 heure
|
||||||
|
daily: 24 * 60 * 60 * 1000, // 24 heures
|
||||||
|
weekly: 7 * 24 * 60 * 60 * 1000, // 7 jours
|
||||||
|
};
|
||||||
|
|
||||||
|
return intervals[interval];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient le prochain moment de synchronisation
|
||||||
|
*/
|
||||||
|
async getNextSyncTime(): Promise<Date | null> {
|
||||||
|
if (!this.isRunning || !this.timer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await this.getConfig();
|
||||||
|
const intervalMs = this.getIntervalMs(config.interval);
|
||||||
|
|
||||||
|
return new Date(Date.now() + intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la configuration du scheduler depuis les user preferences
|
||||||
|
*/
|
||||||
|
private async getConfig(): Promise<JiraSchedulerConfig> {
|
||||||
|
try {
|
||||||
|
const [jiraConfig, schedulerConfig] = await Promise.all([
|
||||||
|
userPreferencesService.getJiraConfig(),
|
||||||
|
userPreferencesService.getJiraSchedulerConfig()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: schedulerConfig.jiraAutoSync &&
|
||||||
|
jiraConfig.enabled &&
|
||||||
|
!!jiraConfig.baseUrl &&
|
||||||
|
!!jiraConfig.email &&
|
||||||
|
!!jiraConfig.apiToken,
|
||||||
|
interval: schedulerConfig.jiraSyncInterval
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting Jira scheduler config:', error);
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
interval: 'daily'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient les stats du planificateur
|
||||||
|
*/
|
||||||
|
async getStatus() {
|
||||||
|
const config = await this.getConfig();
|
||||||
|
const jiraConfig = await userPreferencesService.getJiraConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRunning: this.isRunning,
|
||||||
|
isEnabled: config.enabled,
|
||||||
|
interval: config.interval,
|
||||||
|
nextSync: await this.getNextSyncTime(),
|
||||||
|
jiraConfigured: !!(jiraConfig.baseUrl && jiraConfig.email && jiraConfig.apiToken),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance singleton
|
||||||
|
export const jiraScheduler = new JiraScheduler();
|
||||||
|
|
||||||
|
// Auto-start du scheduler
|
||||||
|
// Démarrer avec un délai pour laisser l'app s'initialiser
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('🚀 Auto-starting Jira scheduler...');
|
||||||
|
jiraScheduler.start();
|
||||||
|
}, 6000); // 6 secondes, après le backup scheduler
|
||||||
@@ -30,7 +30,9 @@ const DEFAULT_PREFERENCES: UserPreferences = {
|
|||||||
email: '',
|
email: '',
|
||||||
apiToken: '',
|
apiToken: '',
|
||||||
ignoredProjects: []
|
ignoredProjects: []
|
||||||
}
|
},
|
||||||
|
jiraAutoSync: false,
|
||||||
|
jiraSyncInterval: 'daily'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,9 +58,29 @@ class UserPreferencesService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// S'assurer que les nouveaux champs existent (migration douce)
|
||||||
|
await this.ensureJiraSchedulerFields();
|
||||||
|
|
||||||
return userPrefs;
|
return userPrefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S'assure que les champs jiraAutoSync et jiraSyncInterval existent
|
||||||
|
*/
|
||||||
|
private async ensureJiraSchedulerFields(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE user_preferences
|
||||||
|
SET jiraAutoSync = COALESCE(jiraAutoSync, ${DEFAULT_PREFERENCES.jiraAutoSync}),
|
||||||
|
jiraSyncInterval = COALESCE(jiraSyncInterval, ${DEFAULT_PREFERENCES.jiraSyncInterval})
|
||||||
|
WHERE id = 'default'
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
// Ignorer les erreurs si les colonnes n'existent pas encore
|
||||||
|
console.debug('Migration douce des champs scheduler Jira:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === FILTRES KANBAN ===
|
// === FILTRES KANBAN ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,22 +238,76 @@ class UserPreferencesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === CONFIGURATION SCHEDULER JIRA ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde les préférences du scheduler Jira
|
||||||
|
*/
|
||||||
|
async saveJiraSchedulerConfig(jiraAutoSync: boolean, jiraSyncInterval: 'hourly' | 'daily' | 'weekly'): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userPrefs = await this.getOrCreateUserPreferences();
|
||||||
|
// Utiliser une requête SQL brute temporairement pour éviter les problèmes de types
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE user_preferences
|
||||||
|
SET jiraAutoSync = ${jiraAutoSync}, jiraSyncInterval = ${jiraSyncInterval}
|
||||||
|
WHERE id = ${userPrefs.id}
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la sauvegarde de la config scheduler Jira:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les préférences du scheduler Jira
|
||||||
|
*/
|
||||||
|
async getJiraSchedulerConfig(): Promise<{ jiraAutoSync: boolean; jiraSyncInterval: 'hourly' | 'daily' | 'weekly' }> {
|
||||||
|
try {
|
||||||
|
const userPrefs = await this.getOrCreateUserPreferences();
|
||||||
|
// Utiliser une requête SQL brute pour récupérer les nouveaux champs
|
||||||
|
const result = await prisma.$queryRaw<Array<{ jiraAutoSync: number; jiraSyncInterval: string }>>`
|
||||||
|
SELECT jiraAutoSync, jiraSyncInterval FROM user_preferences WHERE id = ${userPrefs.id}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
return {
|
||||||
|
jiraAutoSync: Boolean(result[0].jiraAutoSync),
|
||||||
|
jiraSyncInterval: (result[0].jiraSyncInterval as 'hourly' | 'daily' | 'weekly') || DEFAULT_PREFERENCES.jiraSyncInterval
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
jiraAutoSync: DEFAULT_PREFERENCES.jiraAutoSync,
|
||||||
|
jiraSyncInterval: DEFAULT_PREFERENCES.jiraSyncInterval
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la récupération de la config scheduler Jira:', error);
|
||||||
|
return {
|
||||||
|
jiraAutoSync: DEFAULT_PREFERENCES.jiraAutoSync,
|
||||||
|
jiraSyncInterval: DEFAULT_PREFERENCES.jiraSyncInterval
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère toutes les préférences utilisateur
|
* Récupère toutes les préférences utilisateur
|
||||||
*/
|
*/
|
||||||
async getAllPreferences(): Promise<UserPreferences> {
|
async getAllPreferences(): Promise<UserPreferences> {
|
||||||
const [kanbanFilters, viewPreferences, columnVisibility, jiraConfig] = await Promise.all([
|
const [kanbanFilters, viewPreferences, columnVisibility, jiraConfig, jiraSchedulerConfig] = await Promise.all([
|
||||||
this.getKanbanFilters(),
|
this.getKanbanFilters(),
|
||||||
this.getViewPreferences(),
|
this.getViewPreferences(),
|
||||||
this.getColumnVisibility(),
|
this.getColumnVisibility(),
|
||||||
this.getJiraConfig()
|
this.getJiraConfig(),
|
||||||
|
this.getJiraSchedulerConfig()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kanbanFilters,
|
kanbanFilters,
|
||||||
viewPreferences,
|
viewPreferences,
|
||||||
columnVisibility,
|
columnVisibility,
|
||||||
jiraConfig
|
jiraConfig,
|
||||||
|
jiraAutoSync: jiraSchedulerConfig.jiraAutoSync,
|
||||||
|
jiraSyncInterval: jiraSchedulerConfig.jiraSyncInterval
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +319,8 @@ class UserPreferencesService {
|
|||||||
this.saveKanbanFilters(preferences.kanbanFilters),
|
this.saveKanbanFilters(preferences.kanbanFilters),
|
||||||
this.saveViewPreferences(preferences.viewPreferences),
|
this.saveViewPreferences(preferences.viewPreferences),
|
||||||
this.saveColumnVisibility(preferences.columnVisibility),
|
this.saveColumnVisibility(preferences.columnVisibility),
|
||||||
this.saveJiraConfig(preferences.jiraConfig)
|
this.saveJiraConfig(preferences.jiraConfig),
|
||||||
|
this.saveJiraSchedulerConfig(preferences.jiraAutoSync, preferences.jiraSyncInterval)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user