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 ; }