diff --git a/clients/jira-config-client.ts b/clients/jira-config-client.ts
new file mode 100644
index 0000000..8879f34
--- /dev/null
+++ b/clients/jira-config-client.ts
@@ -0,0 +1,46 @@
+import { httpClient } from './base/http-client';
+import { JiraConfig } from '@/lib/types';
+
+export interface JiraConfigResponse {
+ jiraConfig: JiraConfig;
+}
+
+export interface SaveJiraConfigRequest {
+ baseUrl: string;
+ email: string;
+ apiToken: string;
+}
+
+export interface SaveJiraConfigResponse {
+ success: boolean;
+ message: string;
+ jiraConfig: JiraConfig;
+}
+
+class JiraConfigClient {
+ private readonly basePath = '/user-preferences/jira-config';
+
+ /**
+ * Récupère la configuration Jira actuelle
+ */
+ async getJiraConfig(): Promise {
+ const response = await httpClient.get(this.basePath);
+ return response.jiraConfig;
+ }
+
+ /**
+ * Sauvegarde la configuration Jira
+ */
+ async saveJiraConfig(config: SaveJiraConfigRequest): Promise {
+ return httpClient.put(this.basePath, config);
+ }
+
+ /**
+ * Supprime la configuration Jira (remet à zéro)
+ */
+ async deleteJiraConfig(): Promise<{ success: boolean; message: string }> {
+ return httpClient.delete(this.basePath);
+ }
+}
+
+export const jiraConfigClient = new JiraConfigClient();
diff --git a/components/settings/JiraConfigForm.tsx b/components/settings/JiraConfigForm.tsx
index aead8ec..e5ef9e5 100644
--- a/components/settings/JiraConfigForm.tsx
+++ b/components/settings/JiraConfigForm.tsx
@@ -1,46 +1,100 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
-import { AppConfig } from '@/lib/config';
+import { useJiraConfig } from '@/hooks/useJiraConfig';
-interface JiraConfigFormProps {
- config: AppConfig;
-}
-
-export function JiraConfigForm({ config }: JiraConfigFormProps) {
+export function JiraConfigForm() {
+ const { config, isLoading: configLoading, saveConfig, deleteConfig } = useJiraConfig();
+
const [formData, setFormData] = useState({
baseUrl: '',
email: '',
apiToken: ''
});
- const [isLoading, setIsLoading] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
+ // Charger les données existantes dans le formulaire
+ useEffect(() => {
+ if (config) {
+ setFormData({
+ baseUrl: config.baseUrl || '',
+ email: config.email || '',
+ apiToken: config.apiToken || ''
+ });
+ }
+ }, [config]);
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- setIsLoading(true);
+ setIsSubmitting(true);
setMessage(null);
try {
- // Note: Dans un vrai environnement, ces variables seraient configurées côté serveur
- // Pour cette démo, on affiche juste un message informatif
- setMessage({
- type: 'success',
- text: 'Configuration sauvegardée. Redémarrez l'application pour appliquer les changements.'
- });
- } catch {
+ const result = await saveConfig(formData);
+
+ if (result.success) {
+ setMessage({
+ type: 'success',
+ text: result.message
+ });
+ } else {
+ setMessage({
+ type: 'error',
+ text: result.message
+ });
+ }
+ } catch (error) {
setMessage({
type: 'error',
- text: 'Erreur lors de la sauvegarde de la configuration'
+ text: error instanceof Error ? error.message : 'Erreur lors de la sauvegarde de la configuration'
});
} finally {
- setIsLoading(false);
+ setIsSubmitting(false);
}
};
- const isJiraConfigured = config.integrations.jira.enabled;
+ const handleDelete = async () => {
+ if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration Jira ?')) {
+ return;
+ }
+
+ setIsSubmitting(true);
+ setMessage(null);
+
+ try {
+ const result = await deleteConfig();
+
+ if (result.success) {
+ setFormData({
+ baseUrl: '',
+ email: '',
+ apiToken: ''
+ });
+ setMessage({
+ type: 'success',
+ text: result.message
+ });
+ } else {
+ setMessage({
+ type: 'error',
+ text: result.message
+ });
+ }
+ } catch (error) {
+ setMessage({
+ type: 'error',
+ text: error instanceof Error ? error.message : 'Erreur lors de la suppression de la configuration'
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const isJiraConfigured = config?.enabled && (config?.baseUrl || config?.email);
+ const isLoading = configLoading || isSubmitting;
return (
@@ -67,19 +121,19 @@ export function JiraConfigForm({ config }: JiraConfigFormProps) {
URL de base:{' '}
- {config.integrations.jira.baseUrl || 'Non définie'}
+ {config?.baseUrl || 'Non définie'}
Email:{' '}
- {config.integrations.jira.email || 'Non défini'}
+ {config?.email || 'Non défini'}
Token API:{' '}
- {config.integrations.jira.apiToken ? '••••••••' : 'Non défini'}
+ {config?.apiToken ? '••••••••' : 'Non défini'}
@@ -147,13 +201,27 @@ export function JiraConfigForm({ config }: JiraConfigFormProps) {
-
+
+
+
+ {isJiraConfigured && (
+
+ )}
+
{message && (
diff --git a/components/settings/SettingsPageClient.tsx b/components/settings/SettingsPageClient.tsx
index 7a1f76b..f9142bc 100644
--- a/components/settings/SettingsPageClient.tsx
+++ b/components/settings/SettingsPageClient.tsx
@@ -6,13 +6,10 @@ import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { JiraConfigForm } from '@/components/settings/JiraConfigForm';
import { JiraSync } from '@/components/jira/JiraSync';
import { JiraLogs } from '@/components/jira/JiraLogs';
-import { AppConfig } from '@/lib/config';
+import { useJiraConfig } from '@/hooks/useJiraConfig';
-interface SettingsPageClientProps {
- config: AppConfig;
-}
-
-export function SettingsPageClient({ config }: SettingsPageClientProps) {
+export function SettingsPageClient() {
+ const { config: jiraConfig } = useJiraConfig();
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'advanced'>('general');
const tabs = [
@@ -97,14 +94,14 @@ export function SettingsPageClient({ config }: SettingsPageClientProps) {
-
+
{/* Colonne 2: Actions et Logs */}
- {config.integrations.jira.enabled && (
+ {jiraConfig?.enabled && (
<>
diff --git a/hooks/useJiraConfig.ts b/hooks/useJiraConfig.ts
new file mode 100644
index 0000000..3f4ba69
--- /dev/null
+++ b/hooks/useJiraConfig.ts
@@ -0,0 +1,80 @@
+import { useState, useEffect } from 'react';
+import { jiraConfigClient, SaveJiraConfigRequest } from '@/clients/jira-config-client';
+import { JiraConfig } from '@/lib/types';
+
+export function useJiraConfig() {
+ const [config, setConfig] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Charger la config au montage
+ useEffect(() => {
+ loadConfig();
+ }, []);
+
+ const loadConfig = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+ const jiraConfig = await jiraConfigClient.getJiraConfig();
+ setConfig(jiraConfig);
+ } catch (err) {
+ console.error('Erreur lors du chargement de la config Jira:', err);
+ setError('Erreur lors du chargement de la configuration');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const saveConfig = async (configData: SaveJiraConfigRequest) => {
+ try {
+ setError(null);
+ const response = await jiraConfigClient.saveJiraConfig(configData);
+
+ if (response.success) {
+ setConfig(response.jiraConfig);
+ return { success: true, message: response.message };
+ } else {
+ setError('Erreur lors de la sauvegarde');
+ return { success: false, message: 'Erreur lors de la sauvegarde' };
+ }
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la sauvegarde de la configuration';
+ setError(errorMessage);
+ return { success: false, message: errorMessage };
+ }
+ };
+
+ const deleteConfig = async () => {
+ try {
+ setError(null);
+ const response = await jiraConfigClient.deleteJiraConfig();
+
+ if (response.success) {
+ setConfig({
+ baseUrl: '',
+ email: '',
+ apiToken: '',
+ enabled: false
+ });
+ return { success: true, message: response.message };
+ } else {
+ setError('Erreur lors de la suppression');
+ return { success: false, message: 'Erreur lors de la suppression' };
+ }
+ } catch (err) {
+ const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la suppression de la configuration';
+ setError(errorMessage);
+ return { success: false, message: errorMessage };
+ }
+ };
+
+ return {
+ config,
+ isLoading,
+ error,
+ saveConfig,
+ deleteConfig,
+ refetch: loadConfig
+ };
+}
diff --git a/lib/types.ts b/lib/types.ts
index 864ca22..47ba8a8 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -81,6 +81,7 @@ export interface ColumnVisibility {
export interface JiraConfig {
baseUrl?: string;
email?: string;
+ apiToken?: string;
enabled: boolean;
}
diff --git a/prisma/migrations/20250917154746_add_jira_config_to_user_preferences/migration.sql b/prisma/migrations/20250917154746_add_jira_config_to_user_preferences/migration.sql
new file mode 100644
index 0000000..3385839
--- /dev/null
+++ b/prisma/migrations/20250917154746_add_jira_config_to_user_preferences/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "user_preferences" ADD COLUMN "jiraConfig" TEXT;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 06bcc9c..49593d7 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -98,6 +98,9 @@ model UserPreferences {
// Visibilité des colonnes (JSON)
columnVisibility Json?
+ // Configuration Jira (JSON)
+ jiraConfig Json?
+
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
diff --git a/services/user-preferences.ts b/services/user-preferences.ts
index d22f388..dc1f487 100644
--- a/services/user-preferences.ts
+++ b/services/user-preferences.ts
@@ -24,7 +24,10 @@ const DEFAULT_PREFERENCES: UserPreferences = {
hiddenStatuses: []
},
jiraConfig: {
- enabled: false
+ enabled: false,
+ baseUrl: '',
+ email: '',
+ apiToken: ''
}
};
@@ -47,6 +50,7 @@ class UserPreferencesService {
kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters,
viewPreferences: DEFAULT_PREFERENCES.viewPreferences,
columnVisibility: DEFAULT_PREFERENCES.columnVisibility,
+ jiraConfig: DEFAULT_PREFERENCES.jiraConfig as any,
}
});
@@ -165,15 +169,43 @@ class UserPreferencesService {
}
}
+ // === CONFIGURATION JIRA ===
+
/**
- * Récupère la configuration Jira depuis les variables d'environnement
+ * Sauvegarde la configuration Jira
+ */
+ async saveJiraConfig(config: JiraConfig): Promise {
+ try {
+ const userPrefs = await this.getOrCreateUserPreferences();
+ await prisma.userPreferences.update({
+ where: { id: userPrefs.id },
+ data: { jiraConfig: config as any }
+ });
+ } catch (error) {
+ console.warn('Erreur lors de la sauvegarde de la config Jira:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Récupère la configuration Jira depuis la base de données avec fallback sur les variables d'environnement
*/
async getJiraConfig(): Promise {
try {
+ const userPrefs = await this.getOrCreateUserPreferences();
+ const dbConfig = (userPrefs as any).jiraConfig as JiraConfig | null;
+
+ // Si config en DB, l'utiliser
+ if (dbConfig && (dbConfig.baseUrl || dbConfig.email || dbConfig.apiToken)) {
+ return { ...DEFAULT_PREFERENCES.jiraConfig, ...dbConfig };
+ }
+
+ // Sinon fallback sur les variables d'environnement (existant)
const config = getConfig();
return {
baseUrl: config.integrations.jira.baseUrl,
email: config.integrations.jira.email,
+ apiToken: '', // On ne retourne pas le token des env vars pour la sécurité
enabled: config.integrations.jira.enabled
};
} catch (error) {
@@ -202,14 +234,14 @@ class UserPreferencesService {
}
/**
- * Sauvegarde toutes les préférences utilisateur (jiraConfig ignorée car elle vient des env vars)
+ * Sauvegarde toutes les préférences utilisateur
*/
async saveAllPreferences(preferences: UserPreferences): Promise {
await Promise.all([
this.saveKanbanFilters(preferences.kanbanFilters),
this.saveViewPreferences(preferences.viewPreferences),
- this.saveColumnVisibility(preferences.columnVisibility)
- // jiraConfig n'est pas sauvegardée car elle vient des variables d'environnement
+ this.saveColumnVisibility(preferences.columnVisibility),
+ this.saveJiraConfig(preferences.jiraConfig)
]);
}
@@ -225,6 +257,7 @@ class UserPreferencesService {
kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters,
viewPreferences: DEFAULT_PREFERENCES.viewPreferences,
columnVisibility: DEFAULT_PREFERENCES.columnVisibility,
+ jiraConfig: DEFAULT_PREFERENCES.jiraConfig as any,
}
});
} catch (error) {
diff --git a/src/app/api/user-preferences/jira-config/route.ts b/src/app/api/user-preferences/jira-config/route.ts
new file mode 100644
index 0000000..54bd097
--- /dev/null
+++ b/src/app/api/user-preferences/jira-config/route.ts
@@ -0,0 +1,110 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { userPreferencesService } from '@/services/user-preferences';
+import { JiraConfig } from '@/lib/types';
+
+/**
+ * GET /api/user-preferences/jira-config
+ * Récupère la configuration Jira
+ */
+export async function GET() {
+ try {
+ const jiraConfig = await userPreferencesService.getJiraConfig();
+ return NextResponse.json({ jiraConfig });
+ } catch (error) {
+ console.error('Erreur lors de la récupération de la config Jira:', error);
+ return NextResponse.json(
+ { error: 'Erreur lors de la récupération de la configuration Jira' },
+ { status: 500 }
+ );
+ }
+}
+
+/**
+ * PUT /api/user-preferences/jira-config
+ * Sauvegarde la configuration Jira
+ */
+export async function PUT(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const { baseUrl, email, apiToken } = body;
+
+ // Validation des données requises
+ if (!baseUrl || !email || !apiToken) {
+ return NextResponse.json(
+ { error: 'baseUrl, email et apiToken sont requis' },
+ { status: 400 }
+ );
+ }
+
+ // Validation du format URL
+ try {
+ new URL(baseUrl);
+ } catch {
+ return NextResponse.json(
+ { error: 'baseUrl doit être une URL valide' },
+ { status: 400 }
+ );
+ }
+
+ // Validation du format email
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ return NextResponse.json(
+ { error: 'email doit être un email valide' },
+ { status: 400 }
+ );
+ }
+
+ const jiraConfig: JiraConfig = {
+ baseUrl: baseUrl.trim(),
+ email: email.trim(),
+ apiToken: apiToken.trim(),
+ enabled: true
+ };
+
+ await userPreferencesService.saveJiraConfig(jiraConfig);
+
+ return NextResponse.json({
+ success: true,
+ message: 'Configuration Jira sauvegardée avec succès',
+ jiraConfig: {
+ ...jiraConfig,
+ apiToken: '••••••••' // Masquer le token dans la réponse
+ }
+ });
+ } catch (error) {
+ console.error('Erreur lors de la sauvegarde de la config Jira:', error);
+ return NextResponse.json(
+ { error: 'Erreur lors de la sauvegarde de la configuration Jira' },
+ { status: 500 }
+ );
+ }
+}
+
+/**
+ * DELETE /api/user-preferences/jira-config
+ * Supprime la configuration Jira (remet à zéro)
+ */
+export async function DELETE() {
+ try {
+ const defaultConfig: JiraConfig = {
+ baseUrl: '',
+ email: '',
+ apiToken: '',
+ enabled: false
+ };
+
+ await userPreferencesService.saveJiraConfig(defaultConfig);
+
+ return NextResponse.json({
+ success: true,
+ message: 'Configuration Jira réinitialisée avec succès'
+ });
+ } catch (error) {
+ console.error('Erreur lors de la suppression de la config Jira:', error);
+ return NextResponse.json(
+ { error: 'Erreur lors de la suppression de la configuration Jira' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
index e3fa1b7..306b4d7 100644
--- a/src/app/settings/page.tsx
+++ b/src/app/settings/page.tsx
@@ -1,15 +1,8 @@
import { SettingsPageClient } from '@/components/settings/SettingsPageClient';
-import { getConfig } from '@/lib/config';
// Force dynamic rendering (no static generation)
export const dynamic = 'force-dynamic';
export default async function SettingsPage() {
- const config = getConfig();
-
- return (
-
- );
+ return ;
}