From 14d300c6829117c6708ffaf8872816f84a06fc97 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 17 Sep 2025 08:30:36 +0200 Subject: [PATCH] refactor: userpreferences are now in the DB --- clients/base/http-client.ts | 7 + clients/user-preferences-client.ts | 77 ++++++ components/HomePageClient.tsx | 48 ++-- components/kanban/Board.tsx | 54 ++--- components/kanban/BoardContainer.tsx | 13 +- components/kanban/KanbanFilters.tsx | 10 +- components/kanban/ObjectivesBoard.tsx | 51 ++-- components/kanban/SwimlanesBase.tsx | 51 ++-- hooks/useColumnVisibility.ts | 52 ---- hooks/useDragAndDrop.ts | 28 +++ hooks/useObjectivesCollapse.ts | 39 --- hooks/useObjectivesVisibility.ts | 30 --- lib/types.ts | 29 +++ .../migration.sql | 9 + prisma/schema.prisma | 18 ++ services/user-preferences.ts | 226 +++++++++--------- .../column-visibility/route.ts | 28 +++ .../user-preferences/kanban-filters/route.ts | 76 ++++++ src/app/api/user-preferences/route.ts | 50 ++++ .../view-preferences/route.ts | 28 +++ src/app/page.tsx | 7 +- src/contexts/TasksContext.tsx | 66 +++-- src/contexts/ThemeContext.tsx | 20 +- src/contexts/UserPreferencesContext.tsx | 150 ++++++++++++ 24 files changed, 763 insertions(+), 404 deletions(-) create mode 100644 clients/user-preferences-client.ts delete mode 100644 hooks/useColumnVisibility.ts create mode 100644 hooks/useDragAndDrop.ts delete mode 100644 hooks/useObjectivesCollapse.ts delete mode 100644 hooks/useObjectivesVisibility.ts create mode 100644 prisma/migrations/20250916201629_add_user_preferences/migration.sql create mode 100644 src/app/api/user-preferences/column-visibility/route.ts create mode 100644 src/app/api/user-preferences/kanban-filters/route.ts create mode 100644 src/app/api/user-preferences/route.ts create mode 100644 src/app/api/user-preferences/view-preferences/route.ts create mode 100644 src/contexts/UserPreferencesContext.tsx diff --git a/clients/base/http-client.ts b/clients/base/http-client.ts index 18f0d50..5b4bd06 100644 --- a/clients/base/http-client.ts +++ b/clients/base/http-client.ts @@ -52,6 +52,13 @@ export class HttpClient { }); } + async put(endpoint: string, data?: unknown): Promise { + return this.request(endpoint, { + method: 'PUT', + body: data ? JSON.stringify(data) : undefined, + }); + } + async patch(endpoint: string, data?: unknown): Promise { return this.request(endpoint, { method: 'PATCH', diff --git a/clients/user-preferences-client.ts b/clients/user-preferences-client.ts new file mode 100644 index 0000000..4e3dc49 --- /dev/null +++ b/clients/user-preferences-client.ts @@ -0,0 +1,77 @@ +import { httpClient } from './base/http-client'; +import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility } from '@/lib/types'; + +export interface UserPreferencesResponse { + success: boolean; + data?: UserPreferences; + message?: string; + error?: string; +} + +export interface UserPreferencesUpdateResponse { + success: boolean; + message?: string; + error?: string; +} + +/** + * Client HTTP pour les préférences utilisateur + */ +export const userPreferencesClient = { + /** + * Récupère toutes les préférences utilisateur + */ + async getPreferences(): Promise { + const response = await httpClient.get('/user-preferences'); + + if (!response.success || !response.data) { + throw new Error(response.error || 'Erreur lors de la récupération des préférences'); + } + + 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/components/HomePageClient.tsx b/components/HomePageClient.tsx index 090190c..743774f 100644 --- a/components/HomePageClient.tsx +++ b/components/HomePageClient.tsx @@ -1,12 +1,12 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { KanbanBoardContainer } from '@/components/kanban/BoardContainer'; import { Header } from '@/components/ui/Header'; import { TasksProvider, useTasksContext } from '@/contexts/TasksContext'; -import { Task, Tag, TaskStats } from '@/lib/types'; +import { UserPreferencesProvider, useUserPreferences } from '@/contexts/UserPreferencesContext'; +import { Task, Tag, TaskStats, UserPreferences } from '@/lib/types'; import { CreateTaskData } from '@/clients/tasks-client'; -import { userPreferencesService } from '@/services/user-preferences'; import { CreateTaskForm } from '@/components/forms/CreateTaskForm'; import { Button } from '@/components/ui/Button'; @@ -14,32 +14,26 @@ interface HomePageClientProps { initialTasks: Task[]; initialStats: TaskStats; initialTags: (Tag & { usage: number })[]; + initialPreferences: UserPreferences; } + function HomePageContent() { const { stats, syncing, createTask } = useTasksContext(); - const [showFilters, setShowFilters] = useState(true); - const [showObjectives, setShowObjectives] = useState(true); + const { preferences, updateViewPreferences } = useUserPreferences(); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - // Charger les préférences depuis le service - useEffect(() => { - const viewPreferences = userPreferencesService.getViewPreferences(); - setShowFilters(viewPreferences.showFilters); - setShowObjectives(viewPreferences.showObjectives); - }, []); + // Extraire les préférences du context + const showFilters = preferences.viewPreferences.showFilters; + const showObjectives = preferences.viewPreferences.showObjectives; - // Sauvegarder les préférences via le service + // Handlers pour les toggles (sauvegarde automatique via le context) const handleToggleFilters = () => { - const newValue = !showFilters; - setShowFilters(newValue); - userPreferencesService.updateViewPreferences({ showFilters: newValue }); + updateViewPreferences({ showFilters: !showFilters }); }; const handleToggleObjectives = () => { - const newValue = !showObjectives; - setShowObjectives(newValue); - userPreferencesService.updateViewPreferences({ showObjectives: newValue }); + updateViewPreferences({ showObjectives: !showObjectives }); }; // Handler pour la création de tâche depuis la barre de contrôles @@ -127,14 +121,16 @@ function HomePageContent() { ); } -export function HomePageClient({ initialTasks, initialStats, initialTags }: HomePageClientProps) { +export function HomePageClient({ initialTasks, initialStats, initialTags, initialPreferences }: HomePageClientProps) { return ( - - - + + + + + ); } diff --git a/components/kanban/Board.tsx b/components/kanban/Board.tsx index dac341f..f86e951 100644 --- a/components/kanban/Board.tsx +++ b/components/kanban/Board.tsx @@ -4,16 +4,14 @@ import { Task, TaskStatus } from '@/lib/types'; import { KanbanColumn } from './Column'; import { CreateTaskData } from '@/clients/tasks-client'; import { useMemo, useState } from 'react'; -import { useColumnVisibility } from '@/hooks/useColumnVisibility'; +import { useUserPreferences } from '@/contexts/UserPreferencesContext'; +import { useDragAndDrop } from '@/hooks/useDragAndDrop'; import { getAllStatuses } from '@/lib/status-config'; import { DndContext, DragEndEvent, DragOverlay, - DragStartEvent, - PointerSensor, - useSensor, - useSensors, + DragStartEvent } from '@dnd-kit/core'; import { TaskCard } from './TaskCard'; @@ -30,18 +28,8 @@ interface KanbanBoardProps { export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, onUpdateStatus, compactView = false, visibleStatuses }: KanbanBoardProps) { const [activeTask, setActiveTask] = useState(null); - - // Gestion de la visibilité des colonnes (utilise les props si disponibles) - const { getVisibleStatuses } = useColumnVisibility(); - - // Configuration des capteurs pour le drag & drop - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, // Évite les clics accidentels - }, - }) - ); + const { isColumnVisible } = useUserPreferences(); + const { isMounted, sensors } = useDragAndDrop(); // Organiser les tâches par statut const tasksByStatus = useMemo(() => { const grouped = tasks.reduce((acc, task) => { @@ -66,7 +54,7 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU // Filtrer les colonnes visibles const visibleColumns = visibleStatuses ? allColumns.filter(column => visibleStatuses.includes(column.id)) : - getVisibleStatuses(allColumns); + allColumns.filter(column => isColumnVisible(column.id)); // Gestion du début du drag @@ -94,17 +82,10 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU await onUpdateStatus(taskId, newStatus); }; - return ( - -
- {/* Espacement supérieur */} -
- + const content = ( +
+ {/* Espacement supérieur */} +
{/* Board tech dark */}
@@ -121,9 +102,22 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU /> ))}
+
+ ); -
+ if (!isMounted) { + return content; + } + return ( + + {content} + {/* Overlay pour le drag & drop */} {activeTask ? ( diff --git a/components/kanban/BoardContainer.tsx b/components/kanban/BoardContainer.tsx index c92943e..290c9fa 100644 --- a/components/kanban/BoardContainer.tsx +++ b/components/kanban/BoardContainer.tsx @@ -8,9 +8,9 @@ import { ObjectivesBoard } from './ObjectivesBoard'; import { KanbanFilters } from './KanbanFilters'; import { EditTaskForm } from '@/components/forms/EditTaskForm'; import { useTasksContext } from '@/contexts/TasksContext'; +import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { Task, TaskStatus } from '@/lib/types'; import { UpdateTaskData, CreateTaskData } from '@/clients/tasks-client'; -import { useColumnVisibility } from '@/hooks/useColumnVisibility'; import { getAllStatuses } from '@/lib/status-config'; interface KanbanBoardContainerProps { @@ -20,7 +20,7 @@ interface KanbanBoardContainerProps { export function KanbanBoardContainer({ showFilters = true, - showObjectives = true + showObjectives = true }: KanbanBoardContainerProps = {}) { const { filteredTasks, @@ -35,9 +35,10 @@ export function KanbanBoardContainer({ tags } = useTasksContext(); - const { hiddenStatuses, toggleStatusVisibility, getVisibleStatuses } = useColumnVisibility(); + const { preferences, toggleColumnVisibility, isColumnVisible } = useUserPreferences(); + const allStatuses = getAllStatuses(); - const visibleStatuses = getVisibleStatuses(allStatuses.map(s => ({ id: s.key }))).map(s => s.id); + const visibleStatuses = allStatuses.filter(status => isColumnVisible(status.key)).map(s => s.key); const [editingTask, setEditingTask] = useState(null); const handleEditTask = (task: Task) => { @@ -79,8 +80,8 @@ export function KanbanBoardContainer({ )} diff --git a/components/kanban/KanbanFilters.tsx b/components/kanban/KanbanFilters.tsx index 76262db..52c1abe 100644 --- a/components/kanban/KanbanFilters.tsx +++ b/components/kanban/KanbanFilters.tsx @@ -8,7 +8,7 @@ import { Input } from '@/components/ui/Input'; import { useTasksContext } from '@/contexts/TasksContext'; import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config'; import { SORT_OPTIONS } from '@/lib/sort-config'; -import { useColumnVisibility } from '@/hooks/useColumnVisibility'; +import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { ColumnVisibilityToggle } from './ColumnVisibilityToggle'; export interface KanbanFilters { @@ -32,11 +32,11 @@ interface KanbanFiltersProps { export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsHiddenStatuses, onToggleStatusVisibility }: KanbanFiltersProps) { const { tags: availableTags, regularTasks } = useTasksContext(); - const { hiddenStatuses: localHiddenStatuses, toggleStatusVisibility: localToggleStatusVisibility } = useColumnVisibility(); + const { preferences, toggleColumnVisibility } = useUserPreferences(); - // Utiliser les props si disponibles, sinon utiliser l'état local - const hiddenStatuses = propsHiddenStatuses || localHiddenStatuses; - const toggleStatusVisibility = onToggleStatusVisibility || localToggleStatusVisibility; + // Utiliser les props si disponibles, sinon utiliser le context + const hiddenStatuses = propsHiddenStatuses || new Set(preferences.columnVisibility.hiddenStatuses); + const toggleStatusVisibility = onToggleStatusVisibility || toggleColumnVisibility; const [isExpanded, setIsExpanded] = useState(false); const [isSortExpanded, setIsSortExpanded] = useState(false); const [isSwimlaneModeExpanded, setIsSwimlaneModeExpanded] = useState(false); diff --git a/components/kanban/ObjectivesBoard.tsx b/components/kanban/ObjectivesBoard.tsx index 1372d5d..bedd503 100644 --- a/components/kanban/ObjectivesBoard.tsx +++ b/components/kanban/ObjectivesBoard.tsx @@ -1,7 +1,8 @@ 'use client'; import { useState } from 'react'; -import { useObjectivesCollapse } from '@/hooks/useObjectivesCollapse'; +import { useUserPreferences } from '@/contexts/UserPreferencesContext'; +import { useDragAndDrop } from '@/hooks/useDragAndDrop'; import { Task, TaskStatus } from '@/lib/types'; import { TaskCard } from './TaskCard'; import { Card, CardHeader, CardContent } from '@/components/ui/Card'; @@ -10,10 +11,7 @@ import { DndContext, DragEndEvent, DragOverlay, - DragStartEvent, - PointerSensor, - useSensor, - useSensors, + DragStartEvent } from '@dnd-kit/core'; import { SortableContext, @@ -105,18 +103,11 @@ export function ObjectivesBoard({ compactView = false, pinnedTagName = "Objectifs" }: ObjectivesBoardProps) { - const { isCollapsed, toggleCollapse } = useObjectivesCollapse(); + const { preferences, toggleObjectivesCollapse } = useUserPreferences(); + const isCollapsed = preferences.viewPreferences.objectivesCollapsed; + const { isMounted, sensors } = useDragAndDrop(); const [activeTask, setActiveTask] = useState(null); - // Configuration des sensors pour le drag & drop - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, // Évite les clics accidentels - }, - }) - ); - // Handlers pour le drag & drop const handleDragStart = (event: DragStartEvent) => { const task = tasks.find(t => t.id === event.active.id); @@ -143,20 +134,14 @@ export function ObjectivesBoard({ return null; // Ne rien afficher s'il n'y a pas d'objectifs } - return ( - -
-
+ const content = ( +
+
+ ); + if (!isMounted) { + return content; + } + + return ( + + {content} + {/* Overlay pour le drag & drop */} {activeTask ? ( diff --git a/components/kanban/SwimlanesBase.tsx b/components/kanban/SwimlanesBase.tsx index 5193034..9e97753 100644 --- a/components/kanban/SwimlanesBase.tsx +++ b/components/kanban/SwimlanesBase.tsx @@ -5,17 +5,15 @@ import { TaskCard } from './TaskCard'; import { QuickAddTask } from './QuickAddTask'; import { CreateTaskData } from '@/clients/tasks-client'; import { useState } from 'react'; -import { useColumnVisibility } from '@/hooks/useColumnVisibility'; +import { useUserPreferences } from '@/contexts/UserPreferencesContext'; +import { useDragAndDrop } from '@/hooks/useDragAndDrop'; import { getAllStatuses } from '@/lib/status-config'; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, - closestCenter, - PointerSensor, - useSensor, - useSensors, + closestCenter } from '@dnd-kit/core'; import { SortableContext, @@ -143,19 +141,11 @@ export function SwimlanesBase({ const [showQuickAdd, setShowQuickAdd] = useState<{ [key: string]: boolean }>({}); // Gestion de la visibilité des colonnes - const { getVisibleStatuses } = useColumnVisibility(); + const { isColumnVisible } = useUserPreferences(); + const { isMounted, sensors } = useDragAndDrop(); const allStatuses = getAllStatuses(); const statusesToShow = visibleStatuses || - getVisibleStatuses(allStatuses.map(s => ({ id: s.key }))).map(s => s.id); - - // Configuration des sensors pour le drag & drop - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, - }, - }) - ); + allStatuses.filter(status => isColumnVisible(status.key)).map(s => s.key); // Handlers pour le drag & drop const handleDragStart = (event: DragStartEvent) => { @@ -203,16 +193,10 @@ export function SwimlanesBase({ setShowQuickAdd(prev => ({ ...prev, [columnId]: !prev[columnId] })); }; - return ( - -
- {/* Espacement supérieur */} -
+ const content = ( +
+ {/* Espacement supérieur */} +
{/* Headers des colonnes visibles */} @@ -305,7 +289,21 @@ export function SwimlanesBase({
+ ); + if (!isMounted) { + return content; + } + + return ( + + {content} + {/* Drag overlay */} {activeTask && ( @@ -315,7 +313,6 @@ export function SwimlanesBase({ /> )} - ); } diff --git a/hooks/useColumnVisibility.ts b/hooks/useColumnVisibility.ts deleted file mode 100644 index e25175c..0000000 --- a/hooks/useColumnVisibility.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useState, useEffect } from 'react'; -import { TaskStatus } from '@/lib/types'; -import { userPreferencesService } from '@/services/user-preferences'; -import { getAllStatuses } from '@/lib/status-config'; - -export function useColumnVisibility() { - const [hiddenStatuses, setHiddenStatuses] = useState>(new Set()); - - // Charger les préférences au montage - useEffect(() => { - const saved = userPreferencesService.getColumnVisibility(); - setHiddenStatuses(new Set(saved.hiddenStatuses)); - }, []); - - const toggleStatusVisibility = (status: TaskStatus) => { - setHiddenStatuses(prev => { - const newSet = new Set(prev); - if (newSet.has(status)) { - newSet.delete(status); - } else { - newSet.add(status); - } - - // Sauvegarder dans localStorage - userPreferencesService.saveColumnVisibility({ - hiddenStatuses: Array.from(newSet) - }); - - return newSet; - }); - }; - - const getVisibleStatuses = (statuses: T[]): T[] => { - return statuses.filter(status => !hiddenStatuses.has(status.id)); - }; - - const isStatusVisible = (status: TaskStatus): boolean => { - return !hiddenStatuses.has(status); - }; - - const getAllAvailableStatuses = () => { - return getAllStatuses(); - }; - - return { - hiddenStatuses, - toggleStatusVisibility, - getVisibleStatuses, - isStatusVisible, - getAllAvailableStatuses - }; -} diff --git a/hooks/useDragAndDrop.ts b/hooks/useDragAndDrop.ts new file mode 100644 index 0000000..b36146e --- /dev/null +++ b/hooks/useDragAndDrop.ts @@ -0,0 +1,28 @@ +import { useState, useEffect } from 'react'; +import { useSensors, useSensor, PointerSensor } from '@dnd-kit/core'; + +/** + * Hook pour gérer le drag & drop de manière safe avec SSR + * Désactive le DnD jusqu'à l'hydratation pour éviter les erreurs d'hydratation + */ +export function useDragAndDrop() { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + // Activer le drag & drop après l'hydratation + setIsMounted(true); + }, []); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }) + ); + + return { + isMounted, + sensors + }; +} diff --git a/hooks/useObjectivesCollapse.ts b/hooks/useObjectivesCollapse.ts deleted file mode 100644 index 9faab57..0000000 --- a/hooks/useObjectivesCollapse.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useState, useEffect } from 'react'; - -const STORAGE_KEY = 'towercontrol_objectives_collapsed'; - -export function useObjectivesCollapse() { - const [isCollapsed, setIsCollapsed] = useState(false); - - // Charger l'état au montage - useEffect(() => { - try { - const saved = localStorage.getItem(STORAGE_KEY); - if (saved !== null) { - setIsCollapsed(JSON.parse(saved)); - } - } catch (error) { - console.warn('Erreur lors du chargement de l\'état de collapse des objectifs:', error); - } - }, []); - - const toggleCollapse = () => { - setIsCollapsed(prev => { - const newValue = !prev; - - // Sauvegarder dans localStorage - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(newValue)); - } catch (error) { - console.warn('Erreur lors de la sauvegarde de l\'état de collapse des objectifs:', error); - } - - return newValue; - }); - }; - - return { - isCollapsed, - toggleCollapse - }; -} diff --git a/hooks/useObjectivesVisibility.ts b/hooks/useObjectivesVisibility.ts deleted file mode 100644 index f358177..0000000 --- a/hooks/useObjectivesVisibility.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useState, useEffect } from 'react'; -import { userPreferencesService } from '@/services/user-preferences'; - -export function useObjectivesVisibility() { - const [showObjectives, setShowObjectives] = useState(true); - - // Charger les préférences au montage - useEffect(() => { - const saved = userPreferencesService.getViewPreferences(); - setShowObjectives(saved.showObjectives); - }, []); - - const toggleObjectivesVisibility = () => { - setShowObjectives(prev => { - const newValue = !prev; - - // Sauvegarder dans localStorage - userPreferencesService.updateViewPreferences({ - showObjectives: newValue - }); - - return newValue; - }); - }; - - return { - showObjectives, - toggleObjectivesVisibility - }; -} diff --git a/lib/types.ts b/lib/types.ts index ea586fb..acff822 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -46,6 +46,35 @@ export interface Tag { isPinned?: boolean; // Tag pour objectifs principaux } +// Types pour les préférences utilisateur +export interface KanbanFilters { + search?: string; + tags?: string[]; + priorities?: TaskPriority[]; + showCompleted?: boolean; + sortBy?: string; +} + +export interface ViewPreferences { + compactView: boolean; + swimlanesByTags: boolean; + swimlanesMode?: 'tags' | 'priority'; + showObjectives: boolean; + showFilters: boolean; + objectivesCollapsed: boolean; + theme: 'light' | 'dark'; +} + +export interface ColumnVisibility { + hiddenStatuses: TaskStatus[]; +} + +export interface UserPreferences { + kanbanFilters: KanbanFilters; + viewPreferences: ViewPreferences; + columnVisibility: ColumnVisibility; +} + // Interface pour les logs de synchronisation export interface SyncLog { id: string; diff --git a/prisma/migrations/20250916201629_add_user_preferences/migration.sql b/prisma/migrations/20250916201629_add_user_preferences/migration.sql new file mode 100644 index 0000000..787633a --- /dev/null +++ b/prisma/migrations/20250916201629_add_user_preferences/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "user_preferences" ( + "id" TEXT NOT NULL PRIMARY KEY, + "kanbanFilters" JSONB, + "viewPreferences" JSONB, + "columnVisibility" JSONB, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 74a7755..241d238 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -84,3 +84,21 @@ model DailyCheckbox { @@index([date]) @@map("daily_checkboxes") } + +model UserPreferences { + id String @id @default(cuid()) + + // Filtres Kanban (JSON) + kanbanFilters Json? + + // Préférences de vue (JSON) + viewPreferences Json? + + // Visibilité des colonnes (JSON) + columnVisibility Json? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("user_preferences") +} diff --git a/services/user-preferences.ts b/services/user-preferences.ts index 996ff30..f0b1be5 100644 --- a/services/user-preferences.ts +++ b/services/user-preferences.ts @@ -1,31 +1,5 @@ -import { TaskPriority, TaskStatus } from '@/lib/types'; - -// Types pour les préférences utilisateur -export interface KanbanFilters { - search?: string; - tags?: string[]; - priorities?: TaskPriority[]; - showCompleted?: boolean; - sortBy?: string; -} - -export interface ViewPreferences { - compactView: boolean; - swimlanesByTags: boolean; - swimlanesMode?: 'tags' | 'priority'; - showObjectives: boolean; - showFilters: boolean; -} - -export interface ColumnVisibility { - hiddenStatuses: TaskStatus[]; -} - -export interface UserPreferences { - kanbanFilters: KanbanFilters; - viewPreferences: ViewPreferences; - columnVisibility: ColumnVisibility; -} +import { TaskStatus, KanbanFilters, ViewPreferences, ColumnVisibility, UserPreferences } from '@/lib/types'; +import { prisma } from './database'; // Valeurs par défaut const DEFAULT_PREFERENCES: UserPreferences = { @@ -33,186 +7,217 @@ const DEFAULT_PREFERENCES: UserPreferences = { search: '', tags: [], priorities: [], - showCompleted: true + showCompleted: true, + sortBy: '' }, viewPreferences: { compactView: false, swimlanesByTags: false, + swimlanesMode: 'tags', showObjectives: true, - showFilters: true + showFilters: true, + objectivesCollapsed: false, + theme: 'dark' }, columnVisibility: { hiddenStatuses: [] } }; -// Clés pour le localStorage -const STORAGE_KEYS = { - KANBAN_FILTERS: 'towercontrol_kanban_filters', - VIEW_PREFERENCES: 'towercontrol_view_preferences', - COLUMN_VISIBILITY: 'towercontrol_column_visibility' -} as const; - /** - * Service pour gérer les préférences utilisateur dans le localStorage + * Service pour gérer les préférences utilisateur en base de données */ -export const userPreferencesService = { +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, + } + }); + + return userPrefs; + } + // === FILTRES KANBAN === /** * Sauvegarde les filtres Kanban */ - saveKanbanFilters(filters: KanbanFilters): void { + async saveKanbanFilters(filters: KanbanFilters): Promise { try { - localStorage.setItem(STORAGE_KEYS.KANBAN_FILTERS, JSON.stringify(filters)); + 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 */ - getKanbanFilters(): KanbanFilters { + async getKanbanFilters(): Promise { try { - const stored = localStorage.getItem(STORAGE_KEYS.KANBAN_FILTERS); - if (stored) { - return { ...DEFAULT_PREFERENCES.kanbanFilters, ...JSON.parse(stored) }; - } + 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; } - return DEFAULT_PREFERENCES.kanbanFilters; - }, + } // === PRÉFÉRENCES DE VUE === /** * Sauvegarde les préférences de vue */ - saveViewPreferences(preferences: ViewPreferences): void { + async saveViewPreferences(preferences: ViewPreferences): Promise { try { - localStorage.setItem(STORAGE_KEYS.VIEW_PREFERENCES, JSON.stringify(preferences)); + 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 */ - getViewPreferences(): ViewPreferences { + async getViewPreferences(): Promise { try { - const stored = localStorage.getItem(STORAGE_KEYS.VIEW_PREFERENCES); - if (stored) { - return { ...DEFAULT_PREFERENCES.viewPreferences, ...JSON.parse(stored) }; - } + 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; } - return DEFAULT_PREFERENCES.viewPreferences; - }, + } // === VISIBILITÉ DES COLONNES === /** * Sauvegarde la visibilité des colonnes */ - saveColumnVisibility(visibility: ColumnVisibility): void { + async saveColumnVisibility(visibility: ColumnVisibility): Promise { try { - localStorage.setItem(STORAGE_KEYS.COLUMN_VISIBILITY, JSON.stringify(visibility)); + 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 */ - getColumnVisibility(): ColumnVisibility { + async getColumnVisibility(): Promise { try { - const stored = localStorage.getItem(STORAGE_KEYS.COLUMN_VISIBILITY); - if (stored) { - return { ...DEFAULT_PREFERENCES.columnVisibility, ...JSON.parse(stored) }; - } + 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; } - return DEFAULT_PREFERENCES.columnVisibility; - }, + } // === MÉTHODES GLOBALES === /** * Récupère toutes les préférences utilisateur */ - getAllPreferences(): UserPreferences { + async getAllPreferences(): Promise { + const [kanbanFilters, viewPreferences, columnVisibility] = await Promise.all([ + this.getKanbanFilters(), + this.getViewPreferences(), + this.getColumnVisibility() + ]); + return { - kanbanFilters: this.getKanbanFilters(), - viewPreferences: this.getViewPreferences(), - columnVisibility: this.getColumnVisibility() + kanbanFilters, + viewPreferences, + columnVisibility }; - }, + } /** * Sauvegarde toutes les préférences utilisateur */ - saveAllPreferences(preferences: UserPreferences): void { - this.saveKanbanFilters(preferences.kanbanFilters); - this.saveViewPreferences(preferences.viewPreferences); - this.saveColumnVisibility(preferences.columnVisibility); - }, + async saveAllPreferences(preferences: UserPreferences): Promise { + await Promise.all([ + this.saveKanbanFilters(preferences.kanbanFilters), + this.saveViewPreferences(preferences.viewPreferences), + this.saveColumnVisibility(preferences.columnVisibility) + ]); + } /** * Remet à zéro toutes les préférences */ - resetAllPreferences(): void { + async resetAllPreferences(): Promise { try { - Object.values(STORAGE_KEYS).forEach(key => { - localStorage.removeItem(key); + 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, + } }); } catch (error) { console.warn('Erreur lors de la remise à zéro des préférences:', error); + throw error; } - }, - - /** - * Vérifie si le localStorage est disponible - */ - isStorageAvailable(): boolean { - try { - const test = '__storage_test__'; - localStorage.setItem(test, test); - localStorage.removeItem(test); - return true; - } catch { - return false; - } - }, + } // === MÉTHODES UTILITAIRES === /** * Met à jour partiellement les filtres Kanban */ - updateKanbanFilters(updates: Partial): void { - const current = this.getKanbanFilters(); - this.saveKanbanFilters({ ...current, ...updates }); - }, + async updateKanbanFilters(updates: Partial): Promise { + const current = await this.getKanbanFilters(); + await this.saveKanbanFilters({ ...current, ...updates }); + } /** * Met à jour partiellement les préférences de vue */ - updateViewPreferences(updates: Partial): void { - const current = this.getViewPreferences(); - this.saveViewPreferences({ ...current, ...updates }); - }, + async updateViewPreferences(updates: Partial): Promise { + const current = await this.getViewPreferences(); + await this.saveViewPreferences({ ...current, ...updates }); + } /** * Met à jour la visibilité d'une colonne spécifique */ - toggleColumnVisibility(status: TaskStatus): void { - const current = this.getColumnVisibility(); + async toggleColumnVisibility(status: TaskStatus): Promise { + const current = await this.getColumnVisibility(); const hiddenStatuses = new Set(current.hiddenStatuses); if (hiddenStatuses.has(status)) { @@ -221,8 +226,11 @@ export const userPreferencesService = { hiddenStatuses.add(status); } - this.saveColumnVisibility({ + await this.saveColumnVisibility({ hiddenStatuses: Array.from(hiddenStatuses) }); } -}; +} + +// Export de l'instance singleton +export const userPreferencesService = new UserPreferencesService(); diff --git a/src/app/api/user-preferences/column-visibility/route.ts b/src/app/api/user-preferences/column-visibility/route.ts new file mode 100644 index 0000000..0697724 --- /dev/null +++ b/src/app/api/user-preferences/column-visibility/route.ts @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..a9d2092 --- /dev/null +++ b/src/app/api/user-preferences/kanban-filters/route.ts @@ -0,0 +1,76 @@ +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/route.ts b/src/app/api/user-preferences/route.ts new file mode 100644 index 0000000..4f3e366 --- /dev/null +++ b/src/app/api/user-preferences/route.ts @@ -0,0 +1,50 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { userPreferencesService } from '@/services/user-preferences'; + +/** + * GET /api/user-preferences - Récupère toutes les préférences utilisateur + */ +export async function GET() { + try { + const preferences = await userPreferencesService.getAllPreferences(); + + return NextResponse.json({ + success: true, + data: preferences + }); + } catch (error) { + console.error('Erreur lors de la récupération des préférences:', error); + return NextResponse.json( + { + success: false, + error: 'Erreur lors de la récupération des préférences' + }, + { status: 500 } + ); + } +} + +/** + * PUT /api/user-preferences - Met à jour toutes les préférences utilisateur + */ +export async function PUT(request: NextRequest) { + try { + const preferences = await request.json(); + + await userPreferencesService.saveAllPreferences(preferences); + + return NextResponse.json({ + success: true, + message: 'Préférences sauvegardées avec succès' + }); + } catch (error) { + console.error('Erreur lors de la sauvegarde des préférences:', error); + return NextResponse.json( + { + success: false, + error: 'Erreur lors de la sauvegarde des préférences' + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user-preferences/view-preferences/route.ts b/src/app/api/user-preferences/view-preferences/route.ts new file mode 100644 index 0000000..868525c --- /dev/null +++ b/src/app/api/user-preferences/view-preferences/route.ts @@ -0,0 +1,28 @@ +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/app/page.tsx b/src/app/page.tsx index 52ac102..d795972 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,6 @@ import { tasksService } from '@/services/tasks'; import { tagsService } from '@/services/tags'; +import { userPreferencesService } from '@/services/user-preferences'; import { HomePageClient } from '@/components/HomePageClient'; // Force dynamic rendering (no static generation) @@ -7,10 +8,11 @@ export const dynamic = 'force-dynamic'; export default async function HomePage() { // SSR - Récupération des données côté serveur - const [initialTasks, initialStats, initialTags] = await Promise.all([ + const [initialTasks, initialStats, initialTags, initialPreferences] = await Promise.all([ tasksService.getTasks(), tasksService.getTaskStats(), - tagsService.getTags() + tagsService.getTags(), + userPreferencesService.getAllPreferences() ]); return ( @@ -18,6 +20,7 @@ export default async function HomePage() { initialTasks={initialTasks} initialStats={initialStats} initialTags={initialTags} + initialPreferences={initialPreferences} /> ); } diff --git a/src/contexts/TasksContext.tsx b/src/contexts/TasksContext.tsx index 50d2ffa..47961da 100644 --- a/src/contexts/TasksContext.tsx +++ b/src/contexts/TasksContext.tsx @@ -1,9 +1,9 @@ 'use client'; -import { createContext, useContext, ReactNode, useState, useMemo, useEffect } from 'react'; +import { createContext, useContext, ReactNode, useMemo } from 'react'; import { useTasks } from '@/hooks/useTasks'; import { useTags } from '@/hooks/useTags'; -import { userPreferencesService } from '@/services/user-preferences'; +import { useUserPreferences } from './UserPreferencesContext'; import { Task, Tag, TaskStats } from '@/lib/types'; import { CreateTaskData, UpdateTaskData, TaskFilters } from '@/clients/tasks-client'; import { KanbanFilters } from '@/components/kanban/KanbanFilters'; @@ -49,48 +49,42 @@ export function TasksProvider({ children, initialTasks, initialStats, initialTag ); const { tags, loading: tagsLoading, error: tagsError } = useTags(initialTags); - - // État des filtres Kanban avec persistance - const [kanbanFilters, setKanbanFilters] = useState({}); + const { preferences, updateKanbanFilters, updateViewPreferences } = useUserPreferences(); - // Charger les préférences au montage - useEffect(() => { - const savedFilters = userPreferencesService.getKanbanFilters(); - const savedViewPrefs = userPreferencesService.getViewPreferences(); - - setKanbanFilters({ - search: savedFilters.search, - tags: savedFilters.tags, - priorities: savedFilters.priorities, - showCompleted: savedFilters.showCompleted, - sortBy: savedFilters.sortBy || createSortKey('priority', 'desc'), // Tri par défaut - compactView: savedViewPrefs.compactView, - swimlanesByTags: savedViewPrefs.swimlanesByTags, - swimlanesMode: savedViewPrefs.swimlanesMode || 'tags' - }); - }, []); + // Construire l'objet KanbanFilters à partir des préférences + const kanbanFilters: KanbanFilters = useMemo(() => ({ + search: preferences.kanbanFilters.search || '', + tags: preferences.kanbanFilters.tags || [], + priorities: preferences.kanbanFilters.priorities || [], + showCompleted: preferences.kanbanFilters.showCompleted ?? true, + sortBy: preferences.kanbanFilters.sortBy || createSortKey('priority', 'desc'), + compactView: preferences.viewPreferences.compactView || false, + swimlanesByTags: preferences.viewPreferences.swimlanesByTags || false, + swimlanesMode: preferences.viewPreferences.swimlanesMode || 'tags' + }), [preferences]); // Fonction pour mettre à jour les filtres avec persistance - const updateKanbanFilters = (newFilters: KanbanFilters) => { - setKanbanFilters(newFilters); - - // Sauvegarder les filtres - userPreferencesService.saveKanbanFilters({ + const setKanbanFilters = async (newFilters: KanbanFilters) => { + // Séparer les vrais filtres des préférences de vue + const kanbanFilterUpdates = { search: newFilters.search, tags: newFilters.tags, priorities: newFilters.priorities, showCompleted: newFilters.showCompleted, sortBy: newFilters.sortBy - }); + }; - // Sauvegarder les préférences de vue - userPreferencesService.saveViewPreferences({ - compactView: newFilters.compactView || false, - swimlanesByTags: newFilters.swimlanesByTags || false, - swimlanesMode: newFilters.swimlanesMode || 'tags', - showObjectives: true, // Toujours visible pour l'instant - showFilters: true // Toujours visible pour l'instant - }); + const viewPreferenceUpdates = { + compactView: newFilters.compactView, + swimlanesByTags: newFilters.swimlanesByTags, + swimlanesMode: newFilters.swimlanesMode + }; + + // Mettre à jour via UserPreferencesContext + await Promise.all([ + updateKanbanFilters(kanbanFilterUpdates), + updateViewPreferences(viewPreferenceUpdates) + ]); }; // Séparer les tâches épinglées (objectifs) des autres et les trier @@ -176,7 +170,7 @@ export function TasksProvider({ children, initialTasks, initialStats, initialTag tagsLoading, tagsError, kanbanFilters, - setKanbanFilters: updateKanbanFilters, + setKanbanFilters, filteredTasks, pinnedTasks }; diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index 7cd41ad..b944f32 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -14,34 +14,22 @@ const ThemeContext = createContext(undefined); interface ThemeProviderProps { children: ReactNode; + initialTheme?: Theme; } -export function ThemeProvider({ children }: ThemeProviderProps) { - const [theme, setThemeState] = useState('dark'); +export function ThemeProvider({ children, initialTheme = 'dark' }: ThemeProviderProps) { + const [theme, setThemeState] = useState(initialTheme); const [mounted, setMounted] = useState(false); // Hydration safe initialization useEffect(() => { setMounted(true); - - // Check localStorage first - const savedTheme = localStorage.getItem('theme') as Theme | null; - if (savedTheme) { - setThemeState(savedTheme); - return; - } - - // Fallback to system preference - if (window.matchMedia('(prefers-color-scheme: light)').matches) { - setThemeState('light'); - } }, []); // Apply theme class to document useEffect(() => { if (mounted) { document.documentElement.className = theme; - localStorage.setItem('theme', theme); } }, [theme, mounted]); @@ -55,7 +43,7 @@ export function ThemeProvider({ children }: ThemeProviderProps) { return ( -
+
{children}
diff --git a/src/contexts/UserPreferencesContext.tsx b/src/contexts/UserPreferencesContext.tsx new file mode 100644 index 0000000..28b2d94 --- /dev/null +++ b/src/contexts/UserPreferencesContext.tsx @@ -0,0 +1,150 @@ +'use client'; + +import { createContext, useContext, ReactNode, useState, useCallback, useEffect } from 'react'; +import { userPreferencesClient } from '@/clients/user-preferences-client'; +import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; + +interface UserPreferencesContextType { + preferences: UserPreferences; + + // Kanban Filters + updateKanbanFilters: (updates: Partial) => Promise; + + // View Preferences + updateViewPreferences: (updates: Partial) => Promise; + toggleObjectivesVisibility: () => Promise; + toggleObjectivesCollapse: () => Promise; + toggleTheme: () => Promise; + setTheme: (theme: 'light' | 'dark') => Promise; + + // Column Visibility + updateColumnVisibility: (updates: Partial) => Promise; + toggleColumnVisibility: (status: TaskStatus) => Promise; + isColumnVisible: (status: TaskStatus) => boolean; +} + +const UserPreferencesContext = createContext(null); + +interface UserPreferencesProviderProps { + children: ReactNode; + initialPreferences: UserPreferences; +} + +export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) { + const [preferences, setPreferences] = useState(initialPreferences); + + // Synchroniser le thème avec le ThemeProvider global (si disponible) + useEffect(() => { + if (typeof window !== 'undefined') { + // Appliquer le thème au document + document.documentElement.className = preferences.viewPreferences.theme; + } + }, [preferences.viewPreferences.theme]); + + // === 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 })); + } + }, [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 })); + } + }, [preferences.viewPreferences]); + + const toggleObjectivesVisibility = useCallback(async () => { + const newValue = !preferences.viewPreferences.showObjectives; + await updateViewPreferences({ showObjectives: newValue }); + }, [preferences.viewPreferences.showObjectives, updateViewPreferences]); + + const toggleObjectivesCollapse = useCallback(async () => { + const newValue = !preferences.viewPreferences.objectivesCollapsed; + await updateViewPreferences({ objectivesCollapsed: newValue }); + }, [preferences.viewPreferences.objectivesCollapsed, updateViewPreferences]); + + const toggleTheme = useCallback(async () => { + const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark'; + await updateViewPreferences({ theme: newTheme }); + }, [preferences.viewPreferences.theme, updateViewPreferences]); + + const setTheme = useCallback(async (theme: 'light' | 'dark') => { + await updateViewPreferences({ theme }); + }, [updateViewPreferences]); + + // === 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 })); + } + }, [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 isColumnVisible = useCallback((status: TaskStatus) => { + return !preferences.columnVisibility.hiddenStatuses.includes(status); + }, [preferences.columnVisibility.hiddenStatuses]); + + const contextValue: UserPreferencesContextType = { + preferences, + updateKanbanFilters, + updateViewPreferences, + toggleObjectivesVisibility, + toggleObjectivesCollapse, + toggleTheme, + setTheme, + updateColumnVisibility, + toggleColumnVisibility, + isColumnVisible + }; + + return ( + + {children} + + ); +} + +export function useUserPreferences() { + const context = useContext(UserPreferencesContext); + if (!context) { + throw new Error('useUserPreferences must be used within a UserPreferencesProvider'); + } + return context; +}