feat: enhance user preferences management with userId integration

- Added `userId` field to `UserPreferences` model in Prisma schema for user-specific preferences.
- Implemented migration to populate existing preferences with the first user.
- Updated user preferences service methods to handle user-specific data retrieval and updates.
- Modified API routes and components to ensure user authentication and fetch preferences based on the authenticated user.
- Enhanced session management in various components to load user preferences accordingly.
This commit is contained in:
Julien Froidefond
2025-09-30 22:15:44 +02:00
parent 17b86b6087
commit 30aaca4877
23 changed files with 381 additions and 124 deletions

View File

@@ -3,6 +3,8 @@
import { JiraAnalyticsService } from '@/services/integrations/jira/analytics';
import { userPreferencesService } from '@/services/core/user-preferences';
import { JiraAnalytics } from '@/lib/types';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export type JiraAnalyticsResult = {
success: boolean;
@@ -15,8 +17,13 @@ export type JiraAnalyticsResult = {
*/
export async function getJiraAnalytics(forceRefresh = false): Promise<JiraAnalyticsResult> {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
// Récupérer la config Jira depuis la base de données
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) {
return {

View File

@@ -3,6 +3,8 @@
import { jiraAnomalyDetection, JiraAnomaly, AnomalyDetectionConfig } from '@/services/integrations/jira/anomaly-detection';
import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integrations/jira/analytics';
import { userPreferencesService } from '@/services/core/user-preferences';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export interface AnomalyDetectionResult {
success: boolean;
@@ -15,8 +17,13 @@ export interface AnomalyDetectionResult {
*/
export async function detectJiraAnomalies(forceRefresh = false): Promise<AnomalyDetectionResult> {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) {
return {

View File

@@ -4,6 +4,8 @@ import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integratio
import { JiraAdvancedFiltersService } from '@/services/integrations/jira/advanced-filters';
import { userPreferencesService } from '@/services/core/user-preferences';
import { AvailableFilters, JiraAnalyticsFilters, JiraAnalytics } from '@/lib/types';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export interface FiltersResult {
success: boolean;
@@ -22,8 +24,13 @@ export interface FilteredAnalyticsResult {
*/
export async function getAvailableJiraFilters(): Promise<FiltersResult> {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) {
return {
@@ -63,8 +70,13 @@ export async function getAvailableJiraFilters(): Promise<FiltersResult> {
*/
export async function getFilteredJiraAnalytics(filters: Partial<JiraAnalyticsFilters>): Promise<FilteredAnalyticsResult> {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) {
return {

View File

@@ -5,6 +5,8 @@ import { userPreferencesService } from '@/services/core/user-preferences';
import { SprintDetails } from '@/components/jira/SprintDetailModal';
import { JiraTask, AssigneeDistribution, StatusDistribution, SprintVelocity } from '@/lib/types';
import { parseDate } from '@/lib/date-utils';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export interface SprintDetailsResult {
success: boolean;
@@ -17,8 +19,13 @@ export interface SprintDetailsResult {
*/
export async function getSprintDetails(sprintName: string): Promise<SprintDetailsResult> {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) {
return {

View File

@@ -4,6 +4,8 @@ import { userPreferencesService } from '@/services/core/user-preferences';
import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
import { Theme } from '@/lib/theme-config';
import { revalidatePath } from 'next/cache';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Met à jour les préférences de vue
@@ -13,7 +15,12 @@ export async function updateViewPreferences(updates: Partial<ViewPreferences>):
error?: string;
}> {
try {
await userPreferencesService.updateViewPreferences(updates);
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateViewPreferences(session.user.id, updates);
revalidatePath('/');
return { success: true };
} catch (error) {
@@ -33,7 +40,12 @@ export async function updateKanbanFilters(updates: Partial<KanbanFilters>): Prom
error?: string;
}> {
try {
await userPreferencesService.updateKanbanFilters(updates);
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateKanbanFilters(session.user.id, updates);
revalidatePath('/kanban');
return { success: true };
} catch (error) {
@@ -53,13 +65,18 @@ export async function updateColumnVisibility(updates: Partial<ColumnVisibility>)
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(session.user.id);
const newColumnVisibility: ColumnVisibility = {
...preferences.columnVisibility,
...updates
};
await userPreferencesService.saveColumnVisibility(newColumnVisibility);
await userPreferencesService.saveColumnVisibility(session.user.id, newColumnVisibility);
revalidatePath('/kanban');
return { success: true };
} catch (error) {
@@ -79,10 +96,15 @@ export async function toggleObjectivesVisibility(): Promise<{
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(session.user.id);
const showObjectives = !preferences.viewPreferences.showObjectives;
await userPreferencesService.updateViewPreferences({ showObjectives });
await userPreferencesService.updateViewPreferences(session.user.id, { showObjectives });
revalidatePath('/');
return { success: true };
} catch (error) {
@@ -102,10 +124,15 @@ export async function toggleObjectivesCollapse(): Promise<{
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(session.user.id);
const collapseObjectives = !preferences.viewPreferences.collapseObjectives;
await userPreferencesService.updateViewPreferences({ collapseObjectives });
await userPreferencesService.updateViewPreferences(session.user.id, { collapseObjectives });
revalidatePath('/');
return { success: true };
} catch (error) {
@@ -125,7 +152,12 @@ export async function setTheme(theme: Theme): Promise<{
error?: string;
}> {
try {
await userPreferencesService.updateViewPreferences({ theme });
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateViewPreferences(session.user.id, { theme });
revalidatePath('/');
return { success: true };
} catch (error) {
@@ -145,10 +177,15 @@ export async function toggleTheme(): Promise<{
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(session.user.id);
const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark';
await userPreferencesService.updateViewPreferences({ theme: newTheme });
await userPreferencesService.updateViewPreferences(session.user.id, { theme: newTheme });
revalidatePath('/');
return { success: true };
} catch (error) {
@@ -168,13 +205,18 @@ export async function toggleFontSize(): Promise<{
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(session.user.id);
const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large'];
const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize);
const nextIndex = (currentIndex + 1) % fontSizes.length;
const newFontSize = fontSizes[nextIndex];
await userPreferencesService.updateViewPreferences({ fontSize: newFontSize });
await userPreferencesService.updateViewPreferences(session.user.id, { fontSize: newFontSize });
revalidatePath('/');
return { success: true };
} catch (error) {
@@ -194,7 +236,12 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(session.user.id);
const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses);
if (hiddenStatuses.has(status)) {
@@ -203,7 +250,7 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{
hiddenStatuses.add(status);
}
await userPreferencesService.saveColumnVisibility({
await userPreferencesService.saveColumnVisibility(session.user.id, {
hiddenStatuses: Array.from(hiddenStatuses)
});

View File

@@ -3,13 +3,20 @@
import { userPreferencesService } from '@/services/core/user-preferences';
import { revalidatePath } from 'next/cache';
import { tfsService, TfsConfig } from '@/services/integrations/tfs';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Sauvegarde la configuration TFS
*/
export async function saveTfsConfig(config: TfsConfig) {
try {
await userPreferencesService.saveTfsConfig(config);
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.saveTfsConfig(session.user.id, config);
// Réinitialiser le service pour prendre en compte la nouvelle config
tfsService.reset();
@@ -34,7 +41,12 @@ export async function saveTfsConfig(config: TfsConfig) {
*/
export async function getTfsConfig() {
try {
const config = await userPreferencesService.getTfsConfig();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const config = await userPreferencesService.getTfsConfig(session.user.id);
return { success: true, data: config };
} catch (error) {
console.error('Erreur récupération config TFS:', error);
@@ -64,7 +76,13 @@ export async function saveTfsSchedulerConfig(
tfsSyncInterval: 'hourly' | 'daily' | 'weekly'
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.saveTfsSchedulerConfig(
session.user.id,
tfsAutoSync,
tfsSyncInterval
);

View File

@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server';
import { createJiraService, JiraService } from '@/services/integrations/jira/jira';
import { userPreferencesService } from '@/services/core/user-preferences';
import { jiraScheduler } from '@/services/integrations/jira/scheduler';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Route POST /api/jira/sync
@@ -10,6 +12,14 @@ import { jiraScheduler } from '@/services/integrations/jira/scheduler';
*/
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
// Vérifier s'il y a des actions spécifiques (scheduler)
const body = await request.json().catch(() => ({}));
const { action, ...params } = body;
@@ -30,6 +40,7 @@ export async function POST(request: Request) {
case 'config':
await userPreferencesService.saveJiraSchedulerConfig(
session.user.id,
params.jiraAutoSync,
params.jiraSyncInterval
);
@@ -50,7 +61,7 @@ export async function POST(request: Request) {
}
// Synchronisation normale (manuelle)
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
let jiraService: JiraService | null = null;
@@ -124,8 +135,16 @@ export async function POST(request: Request) {
*/
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
// Essayer d'abord la config depuis la base de données
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
let jiraService: JiraService | null = null;

View File

@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { createJiraService } from '@/services/integrations/jira/jira';
import { userPreferencesService } from '@/services/core/user-preferences';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* POST /api/jira/validate-project
@@ -8,6 +10,14 @@ import { userPreferencesService } from '@/services/core/user-preferences';
*/
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Non authentifié' },
{ status: 401 }
);
}
const body = await request.json();
const { projectKey } = body;
@@ -19,7 +29,7 @@ export async function POST(request: NextRequest) {
}
// Récupérer la config Jira depuis la base de données
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) {
return NextResponse.json(

View File

@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/core/user-preferences';
import { JiraConfig } from '@/lib/types';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* GET /api/user-preferences/jira-config
@@ -8,7 +10,15 @@ import { JiraConfig } from '@/lib/types';
*/
export async function GET() {
try {
const jiraConfig = await userPreferencesService.getJiraConfig();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Non authentifié' },
{ status: 401 }
);
}
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
return NextResponse.json({ jiraConfig });
} catch (error) {
console.error('Erreur lors de la récupération de la config Jira:', error);
@@ -25,6 +35,14 @@ export async function GET() {
*/
export async function PUT(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Non authentifié' },
{ status: 401 }
);
}
const body = await request.json();
const { baseUrl, email, apiToken, projectKey, ignoredProjects } = body;
@@ -66,7 +84,7 @@ export async function PUT(request: NextRequest) {
: []
};
await userPreferencesService.saveJiraConfig(jiraConfig);
await userPreferencesService.saveJiraConfig(session.user.id, jiraConfig);
return NextResponse.json({
success: true,
@@ -91,6 +109,14 @@ export async function PUT(request: NextRequest) {
*/
export async function DELETE() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Non authentifié' },
{ status: 401 }
);
}
const defaultConfig: JiraConfig = {
baseUrl: '',
email: '',
@@ -99,7 +125,7 @@ export async function DELETE() {
ignoredProjects: []
};
await userPreferencesService.saveJiraConfig(defaultConfig);
await userPreferencesService.saveJiraConfig(session.user.id, defaultConfig);
return NextResponse.json({
success: true,

View File

@@ -1,12 +1,22 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/core/user-preferences';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* GET /api/user-preferences - Récupère toutes les préférences utilisateur
*/
export async function GET() {
try {
const preferences = await userPreferencesService.getAllPreferences();
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
const preferences = await userPreferencesService.getAllPreferences(session.user.id);
return NextResponse.json({
success: true,
@@ -29,9 +39,17 @@ export async function GET() {
*/
export async function PUT(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
const preferences = await request.json();
await userPreferencesService.saveAllPreferences(preferences);
await userPreferencesService.saveAllPreferences(session.user.id, preferences);
return NextResponse.json({
success: true,

View File

@@ -1,13 +1,20 @@
import { userPreferencesService } from '@/services/core/user-preferences';
import { getJiraAnalytics } from '@/actions/jira-analytics';
import { JiraDashboardPageClient } from './JiraDashboardPageClient';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
// Force dynamic rendering
export const dynamic = 'force-dynamic';
export default async function JiraDashboardPage() {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return <div>Non authentifié</div>;
}
// Récupérer la config Jira côté serveur
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id);
// Récupérer les analytics côté serveur (utilise le cache du service)
let initialAnalytics = null;

View File

@@ -8,6 +8,8 @@ import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext";
import { userPreferencesService } from "@/services/core/user-preferences";
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
import { AuthProvider } from "../components/AuthProvider";
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -29,8 +31,14 @@ export default async function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
// Récupérer toutes les préférences côté serveur pour le SSR
const initialPreferences = await userPreferencesService.getAllPreferences();
// Récupérer la session côté serveur pour le SSR
const session = await getServerSession(authOptions);
// Charger les préférences seulement si l'utilisateur est connecté
// Sinon, les préférences par défaut seront chargées côté client
const initialPreferences = session?.user?.id
? await userPreferencesService.getAllPreferences(session.user.id)
: undefined;
return (
<html lang="fr">
@@ -39,12 +47,12 @@ export default async function RootLayout({
>
<AuthProvider>
<ThemeProvider
initialTheme={initialPreferences.viewPreferences.theme}
userPreferredTheme={initialPreferences.viewPreferences.theme === 'light' ? 'dark' : initialPreferences.viewPreferences.theme}
initialTheme={initialPreferences?.viewPreferences.theme || 'light'}
userPreferredTheme={initialPreferences?.viewPreferences.theme === 'light' ? 'dark' : initialPreferences?.viewPreferences.theme || 'light'}
>
<KeyboardShortcutsProvider>
<KeyboardShortcuts />
<JiraConfigProvider config={initialPreferences.jiraConfig}>
<JiraConfigProvider config={initialPreferences?.jiraConfig || { enabled: false }}>
<UserPreferencesProvider initialPreferences={initialPreferences}>
{children}
</UserPreferencesProvider>

View File

@@ -1,15 +1,22 @@
import { userPreferencesService } from '@/services/core/user-preferences';
import { IntegrationsSettingsPageClient } from '@/components/settings/IntegrationsSettingsPageClient';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
// Force dynamic rendering for real-time data
export const dynamic = 'force-dynamic';
export default async function IntegrationsSettingsPage() {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return <div>Non authentifié</div>;
}
// Fetch data server-side
// Preferences are now available via context
const [jiraConfig, tfsConfig] = await Promise.all([
userPreferencesService.getJiraConfig(),
userPreferencesService.getTfsConfig()
userPreferencesService.getJiraConfig(session.user.id),
userPreferencesService.getTfsConfig(session.user.id)
]);
return (

View File

@@ -34,7 +34,7 @@ export function TfsConfigForm() {
try {
setIsLoading(true);
const result = await getTfsConfig();
if (result.success) {
if (result.success && result.data) {
setConfig(result.data);
// Afficher le formulaire par défaut si TFS n'est pas configuré
const isConfigured =

View File

@@ -12,6 +12,7 @@ import {
toggleColumnVisibility as toggleColumnVisibilityAction
} from '@/actions/preferences';
import { useTheme } from './ThemeContext';
import { useSession } from 'next-auth/react';
interface UserPreferencesContextType {
preferences: UserPreferences;
@@ -77,6 +78,38 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || defaultPreferences);
const [isPending, startTransition] = useTransition();
const { theme, toggleTheme: themeToggleTheme, setTheme: themeSetTheme } = useTheme();
const { data: session, status } = useSession();
// Fonction pour charger les préférences côté client
const loadUserPreferences = useCallback(async () => {
if (status === 'loading') return; // Attendre que la session soit chargée
try {
const response = await fetch('/api/user-preferences');
if (response.ok) {
const result = await response.json();
if (result.success) {
setPreferences(result.data);
// Synchroniser le thème avec le ThemeContext
if (result.data.viewPreferences.theme !== theme) {
themeSetTheme(result.data.viewPreferences.theme);
}
}
}
} catch (error) {
console.error('Erreur lors du chargement des préférences:', error);
}
}, [status, theme, themeSetTheme]);
// Recharger les préférences quand la session change (login/logout)
useEffect(() => {
if (status === 'authenticated') {
loadUserPreferences();
} else if (status === 'unauthenticated') {
// Reset aux préférences par défaut quand l'utilisateur se déconnecte
setPreferences(defaultPreferences);
}
}, [status, loadUserPreferences]);
// Synchroniser les préférences avec le thème actuel du ThemeContext
useEffect(() => {

View File

@@ -59,18 +59,16 @@ const DEFAULT_PREFERENCES: UserPreferences = {
* 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() {
private async getOrCreateUserPreferences(userId: string) {
// Utiliser upsert pour éviter les conditions de course
const userPrefs = await prisma.userPreferences.upsert({
where: { id: 'default' }, // ID fixe pour l'utilisateur unique
where: { userId }, // Utiliser userId au lieu de id
update: {}, // Ne rien mettre à jour si existe
create: {
id: 'default',
userId,
kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters,
viewPreferences: DEFAULT_PREFERENCES.viewPreferences,
columnVisibility: DEFAULT_PREFERENCES.columnVisibility,
@@ -79,7 +77,7 @@ class UserPreferencesService {
});
// S'assurer que les nouveaux champs existent (migration douce)
await this.ensureJiraSchedulerFields();
await this.ensureJiraSchedulerFields(userId);
return userPrefs;
}
@@ -87,13 +85,13 @@ class UserPreferencesService {
/**
* S'assure que les champs jiraAutoSync et jiraSyncInterval existent
*/
private async ensureJiraSchedulerFields(): Promise<void> {
private async ensureJiraSchedulerFields(userId: string): 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'
WHERE userId = ${userId}
`;
} catch (error) {
// Ignorer les erreurs si les colonnes n'existent pas encore
@@ -106,11 +104,11 @@ class UserPreferencesService {
/**
* Sauvegarde les filtres Kanban
*/
async saveKanbanFilters(filters: KanbanFilters): Promise<void> {
async saveKanbanFilters(userId: string, filters: KanbanFilters): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
await prisma.userPreferences.update({
where: { id: userPrefs.id },
where: { userId },
data: { kanbanFilters: filters }
});
} catch (error) {
@@ -122,9 +120,9 @@ class UserPreferencesService {
/**
* Récupère les filtres Kanban
*/
async getKanbanFilters(): Promise<KanbanFilters> {
async getKanbanFilters(userId: string): Promise<KanbanFilters> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
const filters = userPrefs.kanbanFilters as KanbanFilters | null;
return { ...DEFAULT_PREFERENCES.kanbanFilters, ...(filters || {}) };
} catch (error) {
@@ -138,11 +136,11 @@ class UserPreferencesService {
/**
* Sauvegarde les préférences de vue
*/
async saveViewPreferences(preferences: ViewPreferences): Promise<void> {
async saveViewPreferences(userId: string, preferences: ViewPreferences): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
await prisma.userPreferences.update({
where: { id: userPrefs.id },
where: { userId },
data: { viewPreferences: preferences }
});
} catch (error) {
@@ -157,9 +155,9 @@ class UserPreferencesService {
/**
* Récupère les préférences de vue
*/
async getViewPreferences(): Promise<ViewPreferences> {
async getViewPreferences(userId: string): Promise<ViewPreferences> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
const preferences = userPrefs.viewPreferences as ViewPreferences | null;
return { ...DEFAULT_PREFERENCES.viewPreferences, ...(preferences || {}) };
} catch (error) {
@@ -176,11 +174,11 @@ class UserPreferencesService {
/**
* Sauvegarde la visibilité des colonnes
*/
async saveColumnVisibility(visibility: ColumnVisibility): Promise<void> {
async saveColumnVisibility(userId: string, visibility: ColumnVisibility): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
await prisma.userPreferences.update({
where: { id: userPrefs.id },
where: { userId },
data: { columnVisibility: visibility },
});
} catch (error) {
@@ -195,9 +193,9 @@ class UserPreferencesService {
/**
* Récupère la visibilité des colonnes
*/
async getColumnVisibility(): Promise<ColumnVisibility> {
async getColumnVisibility(userId: string): Promise<ColumnVisibility> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
const visibility = userPrefs.columnVisibility as ColumnVisibility | null;
return { ...DEFAULT_PREFERENCES.columnVisibility, ...(visibility || {}) };
} catch (error) {
@@ -214,9 +212,9 @@ class UserPreferencesService {
/**
* Récupère uniquement le thème pour le SSR (optimisé)
*/
async getTheme(): Promise<Theme> {
async getTheme(userId: string): Promise<Theme> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
const viewPrefs = userPrefs.viewPreferences as ViewPreferences;
return viewPrefs.theme;
} catch (error) {
@@ -230,11 +228,11 @@ class UserPreferencesService {
/**
* Sauvegarde la configuration Jira
*/
async saveJiraConfig(config: JiraConfig): Promise<void> {
async saveJiraConfig(userId: string, config: JiraConfig): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
await prisma.userPreferences.update({
where: { id: userPrefs.id },
where: { userId },
data: { jiraConfig: config as any } // eslint-disable-line @typescript-eslint/no-explicit-any
});
} catch (error) {
@@ -246,9 +244,9 @@ class UserPreferencesService {
/**
* Récupère la configuration Jira depuis la base de données avec fallback sur les variables d'environnement
*/
async getJiraConfig(): Promise<JiraConfig> {
async getJiraConfig(userId: string): Promise<JiraConfig> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
const dbConfig = userPrefs.jiraConfig as JiraConfig | null;
// Si config en DB, l'utiliser
@@ -278,11 +276,11 @@ class UserPreferencesService {
/**
* Sauvegarde la configuration TFS
*/
async saveTfsConfig(config: TfsConfig): Promise<void> {
async saveTfsConfig(userId: string, config: TfsConfig): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
await prisma.userPreferences.update({
where: { id: userPrefs.id },
where: { userId },
data: { tfsConfig: config as any }, // eslint-disable-line @typescript-eslint/no-explicit-any
});
} catch (error) {
@@ -294,9 +292,9 @@ class UserPreferencesService {
/**
* Récupère la configuration TFS depuis la base de données
*/
async getTfsConfig(): Promise<TfsConfig> {
async getTfsConfig(userId: string): Promise<TfsConfig> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
const dbConfig = userPrefs.tfsConfig as TfsConfig | null;
if (
@@ -319,15 +317,16 @@ class UserPreferencesService {
* Sauvegarde les préférences du scheduler TFS
*/
async saveTfsSchedulerConfig(
userId: string,
tfsAutoSync: boolean,
tfsSyncInterval: 'hourly' | 'daily' | 'weekly'
): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
await prisma.$executeRaw`
UPDATE user_preferences
SET tfsAutoSync = ${tfsAutoSync}, tfsSyncInterval = ${tfsSyncInterval}
WHERE id = ${userPrefs.id}
WHERE userId = ${userId}
`;
} catch (error) {
console.warn(
@@ -341,16 +340,16 @@ class UserPreferencesService {
/**
* Récupère les préférences du scheduler TFS
*/
async getTfsSchedulerConfig(): Promise<{
async getTfsSchedulerConfig(userId: string): Promise<{
tfsAutoSync: boolean;
tfsSyncInterval: 'hourly' | 'daily' | 'weekly';
}> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
const result = await prisma.$queryRaw<
Array<{ tfsAutoSync: number; tfsSyncInterval: string }>
>`
SELECT tfsAutoSync, tfsSyncInterval FROM user_preferences WHERE id = ${userPrefs.id}
SELECT tfsAutoSync, tfsSyncInterval FROM user_preferences WHERE userId = ${userId}
`;
if (result.length > 0) {
@@ -384,16 +383,17 @@ class UserPreferencesService {
* Sauvegarde les préférences du scheduler Jira
*/
async saveJiraSchedulerConfig(
userId: string,
jiraAutoSync: boolean,
jiraSyncInterval: 'hourly' | 'daily' | 'weekly'
): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
// 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}
WHERE userId = ${userId}
`;
} catch (error) {
console.warn(
@@ -407,17 +407,17 @@ class UserPreferencesService {
/**
* Récupère les préférences du scheduler Jira
*/
async getJiraSchedulerConfig(): Promise<{
async getJiraSchedulerConfig(userId: string): Promise<{
jiraAutoSync: boolean;
jiraSyncInterval: 'hourly' | 'daily' | 'weekly';
}> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
// 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}
SELECT jiraAutoSync, jiraSyncInterval FROM user_preferences WHERE userId = ${userPrefs.userId}
`;
if (result.length > 0) {
@@ -445,14 +445,14 @@ class UserPreferencesService {
/**
* Récupère les préférences utilisateur (alias pour getAllPreferences)
*/
async getUserPreferences(): Promise<UserPreferences> {
return this.getAllPreferences();
async getUserPreferences(userId: string): Promise<UserPreferences> {
return this.getAllPreferences(userId);
}
/**
* Récupère toutes les préférences utilisateur
*/
async getAllPreferences(): Promise<UserPreferences> {
async getAllPreferences(userId: string): Promise<UserPreferences> {
const [
kanbanFilters,
viewPreferences,
@@ -462,13 +462,13 @@ class UserPreferencesService {
tfsConfig,
tfsSchedulerConfig,
] = await Promise.all([
this.getKanbanFilters(),
this.getViewPreferences(),
this.getColumnVisibility(),
this.getJiraConfig(),
this.getJiraSchedulerConfig(),
this.getTfsConfig(),
this.getTfsSchedulerConfig(),
this.getKanbanFilters(userId),
this.getViewPreferences(userId),
this.getColumnVisibility(userId),
this.getJiraConfig(userId),
this.getJiraSchedulerConfig(userId),
this.getTfsConfig(userId),
this.getTfsSchedulerConfig(userId),
]);
return {
@@ -487,18 +487,20 @@ class UserPreferencesService {
/**
* Sauvegarde toutes les préférences utilisateur
*/
async saveAllPreferences(preferences: UserPreferences): Promise<void> {
async saveAllPreferences(userId: string, preferences: UserPreferences): Promise<void> {
await Promise.all([
this.saveKanbanFilters(preferences.kanbanFilters),
this.saveViewPreferences(preferences.viewPreferences),
this.saveColumnVisibility(preferences.columnVisibility),
this.saveJiraConfig(preferences.jiraConfig),
this.saveKanbanFilters(userId, preferences.kanbanFilters),
this.saveViewPreferences(userId, preferences.viewPreferences),
this.saveColumnVisibility(userId, preferences.columnVisibility),
this.saveJiraConfig(userId, preferences.jiraConfig),
this.saveJiraSchedulerConfig(
userId,
preferences.jiraAutoSync,
preferences.jiraSyncInterval
),
this.saveTfsConfig(preferences.tfsConfig),
this.saveTfsConfig(userId, preferences.tfsConfig),
this.saveTfsSchedulerConfig(
userId,
preferences.tfsAutoSync,
preferences.tfsSyncInterval
),
@@ -508,11 +510,11 @@ class UserPreferencesService {
/**
* Remet à zéro toutes les préférences
*/
async resetAllPreferences(): Promise<void> {
async resetAllPreferences(userId: string): Promise<void> {
try {
const userPrefs = await this.getOrCreateUserPreferences();
const userPrefs = await this.getOrCreateUserPreferences(userId);
await prisma.userPreferences.update({
where: { id: userPrefs.id },
where: { userId: userPrefs.userId },
data: {
kanbanFilters: DEFAULT_PREFERENCES.kanbanFilters,
viewPreferences: DEFAULT_PREFERENCES.viewPreferences,
@@ -531,24 +533,24 @@ class UserPreferencesService {
/**
* Met à jour partiellement les filtres Kanban
*/
async updateKanbanFilters(updates: Partial<KanbanFilters>): Promise<void> {
const current = await this.getKanbanFilters();
await this.saveKanbanFilters({ ...current, ...updates });
async updateKanbanFilters(userId: string, updates: Partial<KanbanFilters>): Promise<void> {
const current = await this.getKanbanFilters(userId);
await this.saveKanbanFilters(userId, { ...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 });
async updateViewPreferences(userId: string, updates: Partial<ViewPreferences>): Promise<void> {
const current = await this.getViewPreferences(userId);
await this.saveViewPreferences(userId, { ...current, ...updates });
}
/**
* Met à jour la visibilité d'une colonne spécifique
*/
async toggleColumnVisibility(status: TaskStatus): Promise<void> {
const current = await this.getColumnVisibility();
async toggleColumnVisibility(userId: string, status: TaskStatus): Promise<void> {
const current = await this.getColumnVisibility(userId);
const hiddenStatuses = new Set(current.hiddenStatuses);
if (hiddenStatuses.has(status)) {
@@ -557,7 +559,7 @@ class UserPreferencesService {
hiddenStatuses.add(status);
}
await this.saveColumnVisibility({
await this.saveColumnVisibility(userId, {
hiddenStatuses: Array.from(hiddenStatuses)
});
}

View File

@@ -55,7 +55,9 @@ export class BackupService {
*/
private async loadConfigFromDB(): Promise<void> {
try {
const preferences = await userPreferencesService.getAllPreferences();
// Pour le service de backup, on utilise un userId par défaut
// car il n'a pas accès à la session
const preferences = await userPreferencesService.getAllPreferences('default');
if (preferences.viewPreferences && typeof preferences.viewPreferences === 'object') {
const backupConfig = (preferences.viewPreferences as Record<string, unknown>).backupConfig;
if (backupConfig) {
@@ -75,15 +77,15 @@ export class BackupService {
// Pour l'instant, on stocke la config backup en tant que JSON dans viewPreferences
// TODO: Ajouter un champ dédié dans le schéma pour la config backup
await prisma.userPreferences.upsert({
where: { id: 'default' },
where: { userId: 'default' },
update: {
viewPreferences: JSON.parse(JSON.stringify({
...(await userPreferencesService.getViewPreferences()),
...(await userPreferencesService.getViewPreferences('default')),
backupConfig: this.config
}))
},
create: {
id: 'default',
userId: 'default',
kanbanFilters: {},
viewPreferences: JSON.parse(JSON.stringify({ backupConfig: this.config })),
columnVisibility: {},

View File

@@ -28,7 +28,8 @@ export class JiraScheduler {
}
// Vérifier que Jira est configuré
const jiraConfig = await userPreferencesService.getJiraConfig();
// Pour les services système, on utilise un userId par défaut
const jiraConfig = await userPreferencesService.getJiraConfig('default');
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) {
console.log('⚠️ Jira not configured, scheduler cannot start');
return;
@@ -84,7 +85,7 @@ export class JiraScheduler {
console.log('🔄 Starting scheduled Jira sync...');
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig('default');
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) {
console.log('⚠️ Jira config incomplete, skipping scheduled sync');
@@ -154,8 +155,8 @@ export class JiraScheduler {
private async getConfig(): Promise<JiraSchedulerConfig> {
try {
const [jiraConfig, schedulerConfig] = await Promise.all([
userPreferencesService.getJiraConfig(),
userPreferencesService.getJiraSchedulerConfig()
userPreferencesService.getJiraConfig('default'),
userPreferencesService.getJiraSchedulerConfig('default')
]);
return {
@@ -180,7 +181,7 @@ export class JiraScheduler {
*/
async getStatus() {
const config = await this.getConfig();
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig('default');
return {
isRunning: this.isRunning,

View File

@@ -1054,7 +1054,7 @@ class TfsServiceInstance extends TfsService {
}
private async getConfig(): Promise<TfsConfig> {
const userConfig = await userPreferencesService.getTfsConfig();
const userConfig = await userPreferencesService.getTfsConfig('default');
return userConfig;
}