refactor: userpreferences are now in the DB

This commit is contained in:
Julien Froidefond
2025-09-17 08:30:36 +02:00
parent 4f137455f4
commit 14d300c682
24 changed files with 763 additions and 404 deletions

View File

@@ -52,6 +52,13 @@ export class HttpClient {
});
}
async put<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
});
}
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PATCH',

View File

@@ -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<UserPreferences> {
const response = await httpClient.get<UserPreferencesResponse>('/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<void> {
const response = await httpClient.put<UserPreferencesUpdateResponse>('/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<KanbanFilters>): Promise<void> {
const response = await httpClient.patch<UserPreferencesUpdateResponse>('/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<ViewPreferences>): Promise<void> {
const response = await httpClient.patch<UserPreferencesUpdateResponse>('/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<ColumnVisibility>): Promise<void> {
const response = await httpClient.patch<UserPreferencesUpdateResponse>('/user-preferences/column-visibility', visibility);
if (!response.success) {
throw new Error(response.error || 'Erreur lors de la mise à jour de la visibilité des colonnes');
}
}
};

View File

@@ -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 (
<TasksProvider
initialTasks={initialTasks}
initialStats={initialStats}
initialTags={initialTags}
>
<HomePageContent />
</TasksProvider>
<UserPreferencesProvider initialPreferences={initialPreferences}>
<TasksProvider
initialTasks={initialTasks}
initialStats={initialStats}
initialTags={initialTags}
>
<HomePageContent />
</TasksProvider>
</UserPreferencesProvider>
);
}

View File

@@ -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<Task | null>(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 (
<DndContext
id="kanban-board"
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="h-full flex flex-col bg-[var(--background)]">
{/* Espacement supérieur */}
<div className="pt-4"></div>
const content = (
<div className="h-full flex flex-col bg-[var(--background)]">
{/* Espacement supérieur */}
<div className="pt-4"></div>
{/* Board tech dark */}
<div className="flex-1 flex gap-6 overflow-x-auto p-6">
@@ -121,9 +102,22 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
/>
))}
</div>
</div>
);
</div>
if (!isMounted) {
return content;
}
return (
<DndContext
id="kanban-board"
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{content}
{/* Overlay pour le drag & drop */}
<DragOverlay>
{activeTask ? (

View File

@@ -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<Task | null>(null);
const handleEditTask = (task: Task) => {
@@ -79,8 +80,8 @@ export function KanbanBoardContainer({
<KanbanFilters
filters={kanbanFilters}
onFiltersChange={setKanbanFilters}
hiddenStatuses={hiddenStatuses}
onToggleStatusVisibility={toggleStatusVisibility}
hiddenStatuses={new Set(preferences.columnVisibility.hiddenStatuses)}
onToggleStatusVisibility={toggleColumnVisibility}
/>
)}

View File

@@ -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);

View File

@@ -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<Task | null>(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 (
<DndContext
id="objectives-board"
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="bg-[var(--card)]/30 border-b border-[var(--accent)]/30">
<div className="container mx-auto px-6 py-4">
const content = (
<div className="bg-[var(--card)]/30 border-b border-[var(--accent)]/30">
<div className="container mx-auto px-6 py-4">
<Card variant="column" className="border-[var(--accent)]/30 shadow-[var(--accent)]/10">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<button
onClick={toggleCollapse}
onClick={toggleObjectivesCollapse}
className="flex items-center gap-3 hover:bg-[var(--accent)]/20 rounded-lg p-2 -m-2 transition-colors group"
>
<div className="w-3 h-3 bg-[var(--accent)] rounded-full animate-pulse shadow-[var(--accent)]/50 shadow-lg"></div>
@@ -189,7 +174,7 @@ export function ObjectivesBoard({
{/* Bouton collapse séparé pour mobile */}
<button
onClick={toggleCollapse}
onClick={toggleObjectivesCollapse}
className="lg:hidden p-1 hover:bg-[var(--accent)]/20 rounded transition-colors"
aria-label={isCollapsed ? "Développer" : "Réduire"}
>
@@ -261,7 +246,21 @@ export function ObjectivesBoard({
</Card>
</div>
</div>
);
if (!isMounted) {
return content;
}
return (
<DndContext
id="objectives-board"
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{content}
{/* Overlay pour le drag & drop */}
<DragOverlay>
{activeTask ? (

View File

@@ -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 (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex flex-col h-full bg-[var(--background)]">
{/* Espacement supérieur */}
<div className="flex-shrink-0 py-2"></div>
const content = (
<div className="flex flex-col h-full bg-[var(--background)]">
{/* Espacement supérieur */}
<div className="flex-shrink-0 py-2"></div>
{/* Headers des colonnes visibles */}
@@ -305,7 +289,21 @@ export function SwimlanesBase({
</div>
</div>
</div>
);
if (!isMounted) {
return content;
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{content}
{/* Drag overlay */}
<DragOverlay>
{activeTask && (
@@ -315,7 +313,6 @@ export function SwimlanesBase({
/>
)}
</DragOverlay>
</DndContext>
);
}

View File

@@ -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<Set<TaskStatus>>(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 = <T extends { id: TaskStatus }>(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
};
}

28
hooks/useDragAndDrop.ts Normal file
View File

@@ -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
};
}

View File

@@ -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
};
}

View File

@@ -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
};
}

View File

@@ -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;

View File

@@ -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
);

View File

@@ -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")
}

View File

@@ -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<void> {
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<KanbanFilters> {
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<void> {
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<ViewPreferences> {
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<void> {
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<ColumnVisibility> {
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<UserPreferences> {
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<void> {
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<void> {
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<KanbanFilters>): void {
const current = this.getKanbanFilters();
this.saveKanbanFilters({ ...current, ...updates });
},
async updateKanbanFilters(updates: Partial<KanbanFilters>): Promise<void> {
const current = await this.getKanbanFilters();
await this.saveKanbanFilters({ ...current, ...updates });
}
/**
* Met à jour partiellement les préférences de vue
*/
updateViewPreferences(updates: Partial<ViewPreferences>): void {
const current = this.getViewPreferences();
this.saveViewPreferences({ ...current, ...updates });
},
async updateViewPreferences(updates: Partial<ViewPreferences>): Promise<void> {
const current = await this.getViewPreferences();
await this.saveViewPreferences({ ...current, ...updates });
}
/**
* Met à jour la visibilité d'une colonne spécifique
*/
toggleColumnVisibility(status: TaskStatus): void {
const current = this.getColumnVisibility();
async toggleColumnVisibility(status: TaskStatus): Promise<void> {
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();

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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 }
);
}
}

View File

@@ -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}
/>
);
}

View File

@@ -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<KanbanFilters>({});
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
};

View File

@@ -14,34 +14,22 @@ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: ReactNode;
initialTheme?: Theme;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>('dark');
export function ThemeProvider({ children, initialTheme = 'dark' }: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>(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 (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
<div className={mounted ? theme : 'dark'}>
<div className={mounted ? theme : initialTheme}>
{children}
</div>
</ThemeContext.Provider>

View File

@@ -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<KanbanFilters>) => Promise<void>;
// View Preferences
updateViewPreferences: (updates: Partial<ViewPreferences>) => Promise<void>;
toggleObjectivesVisibility: () => Promise<void>;
toggleObjectivesCollapse: () => Promise<void>;
toggleTheme: () => Promise<void>;
setTheme: (theme: 'light' | 'dark') => Promise<void>;
// Column Visibility
updateColumnVisibility: (updates: Partial<ColumnVisibility>) => Promise<void>;
toggleColumnVisibility: (status: TaskStatus) => Promise<void>;
isColumnVisible: (status: TaskStatus) => boolean;
}
const UserPreferencesContext = createContext<UserPreferencesContextType | null>(null);
interface UserPreferencesProviderProps {
children: ReactNode;
initialPreferences: UserPreferences;
}
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
const [preferences, setPreferences] = useState<UserPreferences>(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<KanbanFilters>) => {
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<ViewPreferences>) => {
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<ColumnVisibility>) => {
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 (
<UserPreferencesContext.Provider value={contextValue}>
{children}
</UserPreferencesContext.Provider>
);
}
export function useUserPreferences() {
const context = useContext(UserPreferencesContext);
if (!context) {
throw new Error('useUserPreferences must be used within a UserPreferencesProvider');
}
return context;
}