refactor: userpreferences are now in the DB
This commit is contained in:
@@ -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',
|
||||
|
||||
77
clients/user-preferences-client.ts
Normal file
77
clients/user-preferences-client.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,8 +102,21 @@ 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>
|
||||
|
||||
@@ -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 {
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,6 +246,20 @@ 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>
|
||||
|
||||
@@ -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,6 +289,20 @@ export function SwimlanesBase({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!isMounted) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{content}
|
||||
|
||||
{/* Drag overlay */}
|
||||
<DragOverlay>
|
||||
@@ -315,7 +313,6 @@ export function SwimlanesBase({
|
||||
/>
|
||||
)}
|
||||
</DragOverlay>
|
||||
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
28
hooks/useDragAndDrop.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
29
lib/types.ts
29
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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
28
src/app/api/user-preferences/column-visibility/route.ts
Normal file
28
src/app/api/user-preferences/column-visibility/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
76
src/app/api/user-preferences/kanban-filters/route.ts
Normal file
76
src/app/api/user-preferences/kanban-filters/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
50
src/app/api/user-preferences/route.ts
Normal file
50
src/app/api/user-preferences/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
28
src/app/api/user-preferences/view-preferences/route.ts
Normal file
28
src/app/api/user-preferences/view-preferences/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
const { preferences, updateKanbanFilters, updateViewPreferences } = useUserPreferences();
|
||||
|
||||
// État des filtres Kanban avec persistance
|
||||
const [kanbanFilters, setKanbanFilters] = useState<KanbanFilters>({});
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
150
src/contexts/UserPreferencesContext.tsx
Normal file
150
src/contexts/UserPreferencesContext.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user