diff --git a/TODO.md b/TODO.md index 42bc37d..df25b0d 100644 --- a/TODO.md +++ b/TODO.md @@ -187,15 +187,15 @@ - [ ] **Nettoyage** : Modifier composants Daily pour `useTransition` #### Actions User Preferences (Priorité 3) -- [ ] Créer `actions/preferences.ts` pour les toggles -- [ ] `updateViewPreferences(preferences)` - Préférences d'affichage -- [ ] `updateKanbanFilters(filters)` - Filtres Kanban -- [ ] `updateColumnVisibility(columns)` - Visibilité colonnes -- [ ] `updateTheme(theme)` - Changement de thème -- [ ] Remplacer les hooks par server actions directes -- [ ] **Nettoyage** : Supprimer routes `/api/user-preferences/*` (PUT/PATCH) -- [ ] **Nettoyage** : Simplifier `user-preferences-client.ts` (GET uniquement) -- [ ] **Nettoyage** : Modifier `useUserPreferences.ts` pour server actions +- [x] Créer `actions/preferences.ts` pour les toggles +- [x] `updateViewPreferences(preferences)` - Préférences d'affichage +- [x] `updateKanbanFilters(filters)` - Filtres Kanban +- [x] `updateColumnVisibility(columns)` - Visibilité colonnes +- [x] `updateTheme(theme)` - Changement de thème +- [x] Remplacer les hooks par server actions directes +- [x] **Nettoyage** : Supprimer routes `/api/user-preferences/*` (PUT/PATCH) +- [x] **Nettoyage** : Simplifier `user-preferences-client.ts` (GET uniquement) +- [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions #### Actions Tags (Priorité 4) - [ ] Créer `actions/tags.ts` pour la gestion tags diff --git a/clients/user-preferences-client.ts b/clients/user-preferences-client.ts index 4e3dc49..1cf0cb4 100644 --- a/clients/user-preferences-client.ts +++ b/clients/user-preferences-client.ts @@ -1,5 +1,5 @@ import { httpClient } from './base/http-client'; -import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility } from '@/lib/types'; +import { UserPreferences } from '@/lib/types'; export interface UserPreferencesResponse { success: boolean; @@ -8,14 +8,9 @@ export interface UserPreferencesResponse { error?: string; } -export interface UserPreferencesUpdateResponse { - success: boolean; - message?: string; - error?: string; -} - /** - * Client HTTP pour les préférences utilisateur + * Client HTTP pour les préférences utilisateur (lecture seule) + * Les mutations sont gérées par les server actions dans actions/preferences.ts */ export const userPreferencesClient = { /** @@ -29,49 +24,5 @@ export const userPreferencesClient = { } return response.data; - }, - - /** - * Sauvegarde toutes les préférences utilisateur - */ - async savePreferences(preferences: UserPreferences): Promise { - const response = await httpClient.put('/user-preferences', preferences); - - if (!response.success) { - throw new Error(response.error || 'Erreur lors de la sauvegarde des préférences'); - } - }, - - /** - * Met à jour les filtres Kanban - */ - async updateKanbanFilters(filters: Partial): Promise { - const response = await httpClient.patch('/user-preferences/kanban-filters', filters); - - if (!response.success) { - throw new Error(response.error || 'Erreur lors de la mise à jour des filtres Kanban'); - } - }, - - /** - * Met à jour les préférences de vue - */ - async updateViewPreferences(preferences: Partial): Promise { - const response = await httpClient.patch('/user-preferences/view-preferences', preferences); - - if (!response.success) { - throw new Error(response.error || 'Erreur lors de la mise à jour des préférences de vue'); - } - }, - - /** - * Met à jour la visibilité des colonnes - */ - async updateColumnVisibility(visibility: Partial): Promise { - const response = await httpClient.patch('/user-preferences/column-visibility', visibility); - - if (!response.success) { - throw new Error(response.error || 'Erreur lors de la mise à jour de la visibilité des colonnes'); - } } }; diff --git a/src/actions/preferences.ts b/src/actions/preferences.ts new file mode 100644 index 0000000..8d4bef2 --- /dev/null +++ b/src/actions/preferences.ts @@ -0,0 +1,218 @@ +'use server'; + +import { userPreferencesService } from '@/services/user-preferences'; +import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; +import { revalidatePath } from 'next/cache'; + +/** + * Met à jour les préférences de vue + */ +export async function updateViewPreferences(updates: Partial): Promise<{ + success: boolean; + error?: string; +}> { + try { + await userPreferencesService.updateViewPreferences(updates); + revalidatePath('/'); + return { success: true }; + } catch (error) { + console.error('Erreur updateViewPreferences:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} + +/** + * Met à jour les filtres Kanban + */ +export async function updateKanbanFilters(updates: Partial): Promise<{ + success: boolean; + error?: string; +}> { + try { + await userPreferencesService.updateKanbanFilters(updates); + revalidatePath('/kanban'); + return { success: true }; + } catch (error) { + console.error('Erreur updateKanbanFilters:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} + +/** + * Met à jour la visibilité des colonnes + */ +export async function updateColumnVisibility(updates: Partial): Promise<{ + success: boolean; + error?: string; +}> { + try { + const preferences = await userPreferencesService.getAllPreferences(); + const newColumnVisibility: ColumnVisibility = { + ...preferences.columnVisibility, + ...updates + }; + + await userPreferencesService.saveColumnVisibility(newColumnVisibility); + revalidatePath('/kanban'); + return { success: true }; + } catch (error) { + console.error('Erreur updateColumnVisibility:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} + +/** + * Toggle la visibilité des objectifs + */ +export async function toggleObjectivesVisibility(): Promise<{ + success: boolean; + error?: string; +}> { + try { + const preferences = await userPreferencesService.getAllPreferences(); + const showObjectives = !preferences.viewPreferences.showObjectives; + + await userPreferencesService.updateViewPreferences({ showObjectives }); + revalidatePath('/'); + return { success: true }; + } catch (error) { + console.error('Erreur toggleObjectivesVisibility:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} + +/** + * Toggle le mode collapse des objectifs + */ +export async function toggleObjectivesCollapse(): Promise<{ + success: boolean; + error?: string; +}> { + try { + const preferences = await userPreferencesService.getAllPreferences(); + const collapseObjectives = !preferences.viewPreferences.collapseObjectives; + + await userPreferencesService.updateViewPreferences({ collapseObjectives }); + revalidatePath('/'); + return { success: true }; + } catch (error) { + console.error('Erreur toggleObjectivesCollapse:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} + +/** + * Change le thème (light/dark) + */ +export async function setTheme(theme: 'light' | 'dark'): Promise<{ + success: boolean; + error?: string; +}> { + try { + await userPreferencesService.updateViewPreferences({ theme }); + revalidatePath('/'); + return { success: true }; + } catch (error) { + console.error('Erreur setTheme:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} + +/** + * Toggle le thème entre light et dark + */ +export async function toggleTheme(): Promise<{ + success: boolean; + error?: string; +}> { + try { + const preferences = await userPreferencesService.getAllPreferences(); + const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark'; + + await userPreferencesService.updateViewPreferences({ theme: newTheme }); + revalidatePath('/'); + return { success: true }; + } catch (error) { + console.error('Erreur toggleTheme:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} + +/** + * Toggle la taille de police + */ +export async function toggleFontSize(): Promise<{ + success: boolean; + error?: string; +}> { + try { + const preferences = await userPreferencesService.getAllPreferences(); + 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 }); + revalidatePath('/'); + return { success: true }; + } catch (error) { + console.error('Erreur toggleFontSize:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} + +/** + * Toggle la visibilité d'une colonne Kanban + */ +export async function toggleColumnVisibility(status: TaskStatus): Promise<{ + success: boolean; + error?: string; +}> { + try { + const preferences = await userPreferencesService.getAllPreferences(); + const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses); + + if (hiddenStatuses.has(status)) { + hiddenStatuses.delete(status); + } else { + hiddenStatuses.add(status); + } + + await userPreferencesService.saveColumnVisibility({ + hiddenStatuses: Array.from(hiddenStatuses) + }); + + revalidatePath('/kanban'); + return { success: true }; + } catch (error) { + console.error('Erreur toggleColumnVisibility:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }; + } +} diff --git a/src/app/api/user-preferences/column-visibility/route.ts b/src/app/api/user-preferences/column-visibility/route.ts deleted file mode 100644 index 0697724..0000000 --- a/src/app/api/user-preferences/column-visibility/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { userPreferencesService } from '@/services/user-preferences'; - -/** - * PATCH /api/user-preferences/column-visibility - Met à jour partiellement la visibilité des colonnes - */ -export async function PATCH(request: NextRequest) { - try { - const updates = await request.json(); - - const current = await userPreferencesService.getColumnVisibility(); - await userPreferencesService.saveColumnVisibility({ ...current, ...updates }); - - return NextResponse.json({ - success: true, - message: 'Visibilité des colonnes mise à jour avec succès' - }); - } catch (error) { - console.error('Erreur lors de la mise à jour de la visibilité des colonnes:', error); - return NextResponse.json( - { - success: false, - error: 'Erreur lors de la mise à jour de la visibilité des colonnes' - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/user-preferences/kanban-filters/route.ts b/src/app/api/user-preferences/kanban-filters/route.ts deleted file mode 100644 index a9d2092..0000000 --- a/src/app/api/user-preferences/kanban-filters/route.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { userPreferencesService } from '@/services/user-preferences'; - -/** - * GET /api/user-preferences/kanban-filters - Récupère les filtres Kanban - */ -export async function GET() { - try { - const filters = await userPreferencesService.getKanbanFilters(); - - return NextResponse.json({ - success: true, - data: filters - }); - } catch (error) { - console.error('Erreur lors de la récupération des filtres Kanban:', error); - return NextResponse.json( - { - success: false, - error: 'Erreur lors de la récupération des filtres Kanban' - }, - { status: 500 } - ); - } -} - -/** - * PUT /api/user-preferences/kanban-filters - Met à jour les filtres Kanban - */ -export async function PUT(request: NextRequest) { - try { - const filters = await request.json(); - - await userPreferencesService.saveKanbanFilters(filters); - - return NextResponse.json({ - success: true, - message: 'Filtres Kanban sauvegardés avec succès' - }); - } catch (error) { - console.error('Erreur lors de la sauvegarde des filtres Kanban:', error); - return NextResponse.json( - { - success: false, - error: 'Erreur lors de la sauvegarde des filtres Kanban' - }, - { status: 500 } - ); - } -} - -/** - * PATCH /api/user-preferences/kanban-filters - Met à jour partiellement les filtres Kanban - */ -export async function PATCH(request: NextRequest) { - try { - const updates = await request.json(); - - const current = await userPreferencesService.getKanbanFilters(); - await userPreferencesService.saveKanbanFilters({ ...current, ...updates }); - - return NextResponse.json({ - success: true, - message: 'Filtres Kanban mis à jour avec succès' - }); - } catch (error) { - console.error('Erreur lors de la mise à jour des filtres Kanban:', error); - return NextResponse.json( - { - success: false, - error: 'Erreur lors de la mise à jour des filtres Kanban' - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/user-preferences/view-preferences/route.ts b/src/app/api/user-preferences/view-preferences/route.ts deleted file mode 100644 index 868525c..0000000 --- a/src/app/api/user-preferences/view-preferences/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { userPreferencesService } from '@/services/user-preferences'; - -/** - * PATCH /api/user-preferences/view-preferences - Met à jour partiellement les préférences de vue - */ -export async function PATCH(request: NextRequest) { - try { - const updates = await request.json(); - - const current = await userPreferencesService.getViewPreferences(); - await userPreferencesService.saveViewPreferences({ ...current, ...updates }); - - return NextResponse.json({ - success: true, - message: 'Préférences de vue mises à jour avec succès' - }); - } catch (error) { - console.error('Erreur lors de la mise à jour des préférences de vue:', error); - return NextResponse.json( - { - success: false, - error: 'Erreur lors de la mise à jour des préférences de vue' - }, - { status: 500 } - ); - } -} diff --git a/src/contexts/UserPreferencesContext.tsx b/src/contexts/UserPreferencesContext.tsx index 4e797aa..09f1183 100644 --- a/src/contexts/UserPreferencesContext.tsx +++ b/src/contexts/UserPreferencesContext.tsx @@ -1,26 +1,37 @@ 'use client'; -import { createContext, useContext, ReactNode, useState, useCallback, useEffect } from 'react'; -import { userPreferencesClient } from '@/clients/user-preferences-client'; +import { createContext, useContext, ReactNode, useState, useCallback, useEffect, useTransition } from 'react'; import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; +import { + updateViewPreferences as updateViewPreferencesAction, + updateKanbanFilters as updateKanbanFiltersAction, + updateColumnVisibility as updateColumnVisibilityAction, + toggleObjectivesVisibility as toggleObjectivesVisibilityAction, + toggleObjectivesCollapse as toggleObjectivesCollapseAction, + toggleTheme as toggleThemeAction, + setTheme as setThemeAction, + toggleFontSize as toggleFontSizeAction, + toggleColumnVisibility as toggleColumnVisibilityAction +} from '@/actions/preferences'; interface UserPreferencesContextType { preferences: UserPreferences; + isPending: boolean; // Kanban Filters - updateKanbanFilters: (updates: Partial) => Promise; + updateKanbanFilters: (updates: Partial) => void; // View Preferences - updateViewPreferences: (updates: Partial) => Promise; - toggleObjectivesVisibility: () => Promise; - toggleObjectivesCollapse: () => Promise; - toggleTheme: () => Promise; - setTheme: (theme: 'light' | 'dark') => Promise; - toggleFontSize: () => Promise; + updateViewPreferences: (updates: Partial) => void; + toggleObjectivesVisibility: () => void; + toggleObjectivesCollapse: () => void; + toggleTheme: () => void; + setTheme: (theme: 'light' | 'dark') => void; + toggleFontSize: () => void; // Column Visibility - updateColumnVisibility: (updates: Partial) => Promise; - toggleColumnVisibility: (status: TaskStatus) => Promise; + updateColumnVisibility: (updates: Partial) => void; + toggleColumnVisibility: (status: TaskStatus) => void; isColumnVisible: (status: TaskStatus) => boolean; } @@ -33,6 +44,7 @@ interface UserPreferencesProviderProps { export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) { const [preferences, setPreferences] = useState(initialPreferences); + const [isPending, startTransition] = useTransition(); // Synchroniser le thème avec le ThemeProvider global (si disponible) useEffect(() => { @@ -44,87 +56,114 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr // === KANBAN FILTERS === - const updateKanbanFilters = useCallback(async (updates: Partial) => { - const newFilters = { ...preferences.kanbanFilters, ...updates }; - setPreferences(prev => ({ ...prev, kanbanFilters: newFilters })); - - try { - await userPreferencesClient.updateKanbanFilters(updates); - } catch (error) { - console.warn('Erreur lors de la sauvegarde des filtres Kanban:', error); - // Revert optimistic update on error - setPreferences(prev => ({ ...prev, kanbanFilters: preferences.kanbanFilters })); - } + const updateKanbanFilters = useCallback((updates: Partial) => { + startTransition(async () => { + const originalFilters = preferences.kanbanFilters; + + // Optimistic update + setPreferences(prev => ({ + ...prev, + kanbanFilters: { ...prev.kanbanFilters, ...updates } + })); + + try { + const result = await updateKanbanFiltersAction(updates); + if (!result.success) { + console.error('Erreur server action:', result.error); + setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters })); + } + } catch (error) { + console.error('Erreur lors de la mise à jour des filtres Kanban:', error); + setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters })); + } + }); }, [preferences.kanbanFilters]); // === VIEW PREFERENCES === - const updateViewPreferences = useCallback(async (updates: Partial) => { - const newPreferences = { ...preferences.viewPreferences, ...updates }; - setPreferences(prev => ({ ...prev, viewPreferences: newPreferences })); - - try { - await userPreferencesClient.updateViewPreferences(updates); - } catch (error) { - console.warn('Erreur lors de la sauvegarde des préférences de vue:', error); - // Revert optimistic update on error - setPreferences(prev => ({ ...prev, viewPreferences: preferences.viewPreferences })); - } + const updateViewPreferences = useCallback((updates: Partial) => { + startTransition(async () => { + const originalPreferences = preferences.viewPreferences; + + // Optimistic update + setPreferences(prev => ({ + ...prev, + viewPreferences: { ...prev.viewPreferences, ...updates } + })); + + try { + const result = await updateViewPreferencesAction(updates); + if (!result.success) { + console.error('Erreur server action:', result.error); + setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences })); + } + } catch (error) { + console.error('Erreur lors de la mise à jour des préférences de vue:', error); + setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences })); + } + }); }, [preferences.viewPreferences]); - const toggleObjectivesVisibility = useCallback(async () => { - const newValue = !preferences.viewPreferences.showObjectives; - await updateViewPreferences({ showObjectives: newValue }); - }, [preferences.viewPreferences.showObjectives, updateViewPreferences]); + const toggleObjectivesVisibility = useCallback(() => { + startTransition(async () => { + await toggleObjectivesVisibilityAction(); + }); + }, []); - const toggleObjectivesCollapse = useCallback(async () => { - const newValue = !preferences.viewPreferences.objectivesCollapsed; - await updateViewPreferences({ objectivesCollapsed: newValue }); - }, [preferences.viewPreferences.objectivesCollapsed, updateViewPreferences]); + const toggleObjectivesCollapse = useCallback(() => { + startTransition(async () => { + await toggleObjectivesCollapseAction(); + }); + }, []); - const toggleTheme = useCallback(async () => { - const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark'; - await updateViewPreferences({ theme: newTheme }); - }, [preferences.viewPreferences.theme, updateViewPreferences]); + const toggleTheme = useCallback(() => { + startTransition(async () => { + await toggleThemeAction(); + }); + }, []); - const setTheme = useCallback(async (theme: 'light' | 'dark') => { - await updateViewPreferences({ theme }); - }, [updateViewPreferences]); + const setTheme = useCallback((theme: 'light' | 'dark') => { + startTransition(async () => { + await setThemeAction(theme); + }); + }, []); - const toggleFontSize = useCallback(async () => { - 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 updateViewPreferences({ fontSize: newFontSize }); - }, [preferences.viewPreferences.fontSize, updateViewPreferences]); + const toggleFontSize = useCallback(() => { + startTransition(async () => { + await toggleFontSizeAction(); + }); + }, []); // === COLUMN VISIBILITY === - const updateColumnVisibility = useCallback(async (updates: Partial) => { - const newVisibility = { ...preferences.columnVisibility, ...updates }; - setPreferences(prev => ({ ...prev, columnVisibility: newVisibility })); - - try { - await userPreferencesClient.updateColumnVisibility(updates); - } catch (error) { - console.warn('Erreur lors de la sauvegarde de la visibilité des colonnes:', error); - // Revert optimistic update on error - setPreferences(prev => ({ ...prev, columnVisibility: preferences.columnVisibility })); - } + const updateColumnVisibility = useCallback((updates: Partial) => { + startTransition(async () => { + const originalVisibility = preferences.columnVisibility; + + // Optimistic update + setPreferences(prev => ({ + ...prev, + columnVisibility: { ...prev.columnVisibility, ...updates } + })); + + try { + const result = await updateColumnVisibilityAction(updates); + if (!result.success) { + console.error('Erreur server action:', result.error); + setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility })); + } + } catch (error) { + console.error('Erreur lors de la mise à jour de la visibilité des colonnes:', error); + setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility })); + } + }); }, [preferences.columnVisibility]); - const toggleColumnVisibility = useCallback(async (status: TaskStatus) => { - const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses); - - if (hiddenStatuses.has(status)) { - hiddenStatuses.delete(status); - } else { - hiddenStatuses.add(status); - } - - await updateColumnVisibility({ hiddenStatuses: Array.from(hiddenStatuses) }); - }, [preferences.columnVisibility.hiddenStatuses, updateColumnVisibility]); + const toggleColumnVisibility = useCallback((status: TaskStatus) => { + startTransition(async () => { + await toggleColumnVisibilityAction(status); + }); + }, []); const isColumnVisible = useCallback((status: TaskStatus) => { return !preferences.columnVisibility.hiddenStatuses.includes(status); @@ -132,6 +171,7 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr const contextValue: UserPreferencesContextType = { preferences, + isPending, updateKanbanFilters, updateViewPreferences, toggleObjectivesVisibility,