Files
towercontrol/src/services/user-preferences.ts
Julien Froidefond 723a44df32 feat: TFS Sync
2025-09-22 21:51:12 +02:00

566 lines
16 KiB
TypeScript

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<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 ===
/**
* Sauvegarde les filtres Kanban
*/
async saveKanbanFilters(filters: KanbanFilters): Promise<void> {
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<KanbanFilters> {
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<void> {
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<ViewPreferences> {
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<void> {
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<ColumnVisibility> {
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<void> {
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<JiraConfig> {
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<void> {
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<TfsConfig> {
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<void> {
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<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 les préférences utilisateur (alias pour getAllPreferences)
*/
async getUserPreferences(): Promise<UserPreferences> {
return this.getAllPreferences();
}
/**
* Récupère toutes les préférences utilisateur
*/
async getAllPreferences(): Promise<UserPreferences> {
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<void> {
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<void> {
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<KanbanFilters>): Promise<void> {
const current = await this.getKanbanFilters();
await this.saveKanbanFilters({ ...current, ...updates });
}
/**
* Met à jour partiellement les préférences de vue
*/
async updateViewPreferences(updates: Partial<ViewPreferences>): Promise<void> {
const current = await this.getViewPreferences();
await this.saveViewPreferences({ ...current, ...updates });
}
/**
* Met à jour la visibilité d'une colonne spécifique
*/
async toggleColumnVisibility(status: TaskStatus): Promise<void> {
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();