import { TaskStatus, KanbanFilters, ViewPreferences, ColumnVisibility, UserPreferences, JiraConfig, } from '@/lib/types'; import { TfsConfig } from '@/services/tfs'; import { prisma } from './database'; import { getConfig } from '@/lib/config'; // Valeurs par défaut const DEFAULT_PREFERENCES: UserPreferences = { kanbanFilters: { search: '', tags: [], priorities: [], showCompleted: true, sortBy: '' }, viewPreferences: { compactView: false, swimlanesByTags: false, swimlanesMode: 'tags', showObjectives: true, showFilters: true, objectivesCollapsed: false, theme: 'dark', fontSize: 'medium' }, columnVisibility: { hiddenStatuses: [] }, jiraConfig: { enabled: false, baseUrl: '', email: '', apiToken: '', ignoredProjects: [] }, jiraAutoSync: false, jiraSyncInterval: 'daily', tfsConfig: { enabled: false, organizationUrl: '', projectName: '', personalAccessToken: '', repositories: [], ignoredRepositories: [], }, tfsAutoSync: false, tfsSyncInterval: 'daily', }; /** * Service pour gérer les préférences utilisateur en base de données */ class UserPreferencesService { private readonly USER_ID = 'default'; // Pour l'instant, un seul utilisateur /** * Récupère ou crée l'entrée user preferences (avec upsert pour éviter les doublons) */ private async getOrCreateUserPreferences() { // Utiliser upsert pour éviter les conditions de course const userPrefs = await prisma.userPreferences.upsert({ where: { id: 'default' }, // ID fixe pour l'utilisateur unique update: {}, // Ne rien mettre à jour si existe create: { id: 'default', kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters, viewPreferences: DEFAULT_PREFERENCES.viewPreferences, columnVisibility: DEFAULT_PREFERENCES.columnVisibility, jiraConfig: DEFAULT_PREFERENCES.jiraConfig as any, // eslint-disable-line @typescript-eslint/no-explicit-any } }); // S'assurer que les nouveaux champs existent (migration douce) await this.ensureJiraSchedulerFields(); return userPrefs; } /** * S'assure que les champs jiraAutoSync et jiraSyncInterval existent */ private async ensureJiraSchedulerFields(): Promise { 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 === /** * Sauvegarde les filtres Kanban */ async saveKanbanFilters(filters: KanbanFilters): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); await prisma.userPreferences.update({ where: { id: userPrefs.id }, data: { kanbanFilters: filters } }); } catch (error) { console.warn('Erreur lors de la sauvegarde des filtres Kanban:', error); throw error; } } /** * Récupère les filtres Kanban */ async getKanbanFilters(): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); const filters = userPrefs.kanbanFilters as KanbanFilters | null; return { ...DEFAULT_PREFERENCES.kanbanFilters, ...(filters || {}) }; } catch (error) { console.warn('Erreur lors de la récupération des filtres Kanban:', error); return DEFAULT_PREFERENCES.kanbanFilters; } } // === PRÉFÉRENCES DE VUE === /** * Sauvegarde les préférences de vue */ async saveViewPreferences(preferences: ViewPreferences): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); await prisma.userPreferences.update({ where: { id: userPrefs.id }, data: { viewPreferences: preferences } }); } catch (error) { console.warn( 'Erreur lors de la sauvegarde des préférences de vue:', error ); throw error; } } /** * Récupère les préférences de vue */ async getViewPreferences(): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); const preferences = userPrefs.viewPreferences as ViewPreferences | null; return { ...DEFAULT_PREFERENCES.viewPreferences, ...(preferences || {}) }; } catch (error) { console.warn( 'Erreur lors de la récupération des préférences de vue:', error ); return DEFAULT_PREFERENCES.viewPreferences; } } // === VISIBILITÉ DES COLONNES === /** * Sauvegarde la visibilité des colonnes */ async saveColumnVisibility(visibility: ColumnVisibility): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); await prisma.userPreferences.update({ where: { id: userPrefs.id }, data: { columnVisibility: visibility }, }); } catch (error) { console.warn( 'Erreur lors de la sauvegarde de la visibilité des colonnes:', error ); throw error; } } /** * Récupère la visibilité des colonnes */ async getColumnVisibility(): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); const visibility = userPrefs.columnVisibility as ColumnVisibility | null; return { ...DEFAULT_PREFERENCES.columnVisibility, ...(visibility || {}) }; } catch (error) { console.warn( 'Erreur lors de la récupération de la visibilité des colonnes:', error ); return DEFAULT_PREFERENCES.columnVisibility; } } // === MÉTHODES GLOBALES === /** * Récupère uniquement le thème pour le SSR (optimisé) */ async getTheme(): Promise<'light' | 'dark'> { try { const userPrefs = await this.getOrCreateUserPreferences(); const viewPrefs = userPrefs.viewPreferences as ViewPreferences; return viewPrefs.theme; } catch (error) { console.error('Erreur lors de la récupération du thème:', error); return DEFAULT_PREFERENCES.viewPreferences.theme; // Fallback } } // === CONFIGURATION JIRA === /** * 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 } // eslint-disable-line @typescript-eslint/no-explicit-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.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) { console.warn('Erreur lors de la récupération de la config Jira:', error); return DEFAULT_PREFERENCES.jiraConfig; } } // === CONFIGURATION TFS === /** * Sauvegarde la configuration TFS */ async saveTfsConfig(config: TfsConfig): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); await prisma.userPreferences.update({ where: { id: userPrefs.id }, data: { tfsConfig: config as any }, // eslint-disable-line @typescript-eslint/no-explicit-any }); } catch (error) { console.warn('Erreur lors de la sauvegarde de la config TFS:', error); throw error; } } /** * Récupère la configuration TFS depuis la base de données */ async getTfsConfig(): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); const dbConfig = userPrefs.tfsConfig as TfsConfig | null; if ( dbConfig && (dbConfig.organizationUrl || dbConfig.projectName || dbConfig.personalAccessToken) ) { return { ...DEFAULT_PREFERENCES.tfsConfig, ...dbConfig }; } return DEFAULT_PREFERENCES.tfsConfig; } catch (error) { console.warn('Erreur lors de la récupération de la config TFS:', error); return DEFAULT_PREFERENCES.tfsConfig; } } /** * Sauvegarde les préférences du scheduler TFS */ async saveTfsSchedulerConfig( tfsAutoSync: boolean, tfsSyncInterval: 'hourly' | 'daily' | 'weekly' ): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); await prisma.$executeRaw` UPDATE user_preferences SET tfsAutoSync = ${tfsAutoSync}, tfsSyncInterval = ${tfsSyncInterval} WHERE id = ${userPrefs.id} `; } catch (error) { console.warn( 'Erreur lors de la sauvegarde de la config scheduler TFS:', error ); throw error; } } /** * Récupère les préférences du scheduler TFS */ async getTfsSchedulerConfig(): Promise<{ tfsAutoSync: boolean; tfsSyncInterval: 'hourly' | 'daily' | 'weekly'; }> { try { const userPrefs = await this.getOrCreateUserPreferences(); const result = await prisma.$queryRaw< Array<{ tfsAutoSync: number; tfsSyncInterval: string }> >` SELECT tfsAutoSync, tfsSyncInterval FROM user_preferences WHERE id = ${userPrefs.id} `; if (result.length > 0) { return { tfsAutoSync: Boolean(result[0].tfsAutoSync), tfsSyncInterval: (result[0].tfsSyncInterval as 'hourly' | 'daily' | 'weekly') || DEFAULT_PREFERENCES.tfsSyncInterval, }; } return { tfsAutoSync: DEFAULT_PREFERENCES.tfsAutoSync, tfsSyncInterval: DEFAULT_PREFERENCES.tfsSyncInterval, }; } catch (error) { console.warn( 'Erreur lors de la récupération de la config scheduler TFS:', error ); return { tfsAutoSync: DEFAULT_PREFERENCES.tfsAutoSync, tfsSyncInterval: DEFAULT_PREFERENCES.tfsSyncInterval, }; } } // === CONFIGURATION SCHEDULER JIRA === /** * Sauvegarde les préférences du scheduler Jira */ async saveJiraSchedulerConfig( jiraAutoSync: boolean, jiraSyncInterval: 'hourly' | 'daily' | 'weekly' ): Promise { 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 les préférences utilisateur (alias pour getAllPreferences) */ async getUserPreferences(): Promise { return this.getAllPreferences(); } /** * Récupère toutes les préférences utilisateur */ async getAllPreferences(): Promise { const [ kanbanFilters, viewPreferences, columnVisibility, jiraConfig, jiraSchedulerConfig, tfsConfig, tfsSchedulerConfig, ] = await Promise.all([ this.getKanbanFilters(), this.getViewPreferences(), this.getColumnVisibility(), this.getJiraConfig(), this.getJiraSchedulerConfig(), this.getTfsConfig(), this.getTfsSchedulerConfig(), ]); return { kanbanFilters, viewPreferences, columnVisibility, jiraConfig, jiraAutoSync: jiraSchedulerConfig.jiraAutoSync, jiraSyncInterval: jiraSchedulerConfig.jiraSyncInterval, tfsConfig, tfsAutoSync: tfsSchedulerConfig.tfsAutoSync, tfsSyncInterval: tfsSchedulerConfig.tfsSyncInterval, }; } /** * 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), this.saveJiraConfig(preferences.jiraConfig), this.saveJiraSchedulerConfig( preferences.jiraAutoSync, preferences.jiraSyncInterval ), this.saveTfsConfig(preferences.tfsConfig), this.saveTfsSchedulerConfig( preferences.tfsAutoSync, preferences.tfsSyncInterval ), ]); } /** * Remet à zéro toutes les préférences */ async resetAllPreferences(): Promise { try { const userPrefs = await this.getOrCreateUserPreferences(); await prisma.userPreferences.update({ where: { id: userPrefs.id }, data: { kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters, viewPreferences: DEFAULT_PREFERENCES.viewPreferences, columnVisibility: DEFAULT_PREFERENCES.columnVisibility, jiraConfig: DEFAULT_PREFERENCES.jiraConfig as any, // eslint-disable-line @typescript-eslint/no-explicit-any } }); } catch (error) { console.warn('Erreur lors de la remise à zéro des préférences:', error); throw error; } } // === MÉTHODES UTILITAIRES === /** * Met à jour partiellement les filtres Kanban */ async updateKanbanFilters(updates: Partial): Promise { const current = await this.getKanbanFilters(); await this.saveKanbanFilters({ ...current, ...updates }); } /** * Met à jour partiellement les préférences de vue */ async updateViewPreferences(updates: Partial): Promise { const current = await this.getViewPreferences(); await this.saveViewPreferences({ ...current, ...updates }); } /** * Met à jour la visibilité d'une colonne spécifique */ async toggleColumnVisibility(status: TaskStatus): Promise { const current = await this.getColumnVisibility(); const hiddenStatuses = new Set(current.hiddenStatuses); if (hiddenStatuses.has(status)) { hiddenStatuses.delete(status); } else { hiddenStatuses.add(status); } await this.saveColumnVisibility({ hiddenStatuses: Array.from(hiddenStatuses) }); } } // Export de l'instance singleton export const userPreferencesService = new UserPreferencesService();