feat: refactor ObjectivesBoard and enhance column visibility management
- Replaced local state management in `ObjectivesBoard` with `useObjectivesCollapse` hook for better state handling. - Updated collapse button logic to use the new hook's toggle function, improving code clarity. - Refactored `useColumnVisibility` to load user preferences on mount and persist visibility changes, enhancing user experience. - Integrated user preferences for Kanban filters in `TasksContext`, allowing for persistent filter settings across sessions.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useObjectivesCollapse } from '@/hooks/useObjectivesCollapse';
|
||||||
import { Task } from '@/lib/types';
|
import { Task } from '@/lib/types';
|
||||||
import { TaskCard } from './TaskCard';
|
import { TaskCard } from './TaskCard';
|
||||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
@@ -23,7 +23,7 @@ export function ObjectivesBoard({
|
|||||||
compactView = false,
|
compactView = false,
|
||||||
pinnedTagName = "Objectifs"
|
pinnedTagName = "Objectifs"
|
||||||
}: ObjectivesBoardProps) {
|
}: ObjectivesBoardProps) {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const { isCollapsed, toggleCollapse } = useObjectivesCollapse();
|
||||||
|
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
return null; // Ne rien afficher s'il n'y a pas d'objectifs
|
return null; // Ne rien afficher s'il n'y a pas d'objectifs
|
||||||
@@ -36,7 +36,7 @@ export function ObjectivesBoard({
|
|||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={toggleCollapse}
|
||||||
className="flex items-center gap-3 hover:bg-purple-900/20 rounded-lg p-2 -m-2 transition-colors group"
|
className="flex items-center gap-3 hover:bg-purple-900/20 rounded-lg p-2 -m-2 transition-colors group"
|
||||||
>
|
>
|
||||||
<div className="w-3 h-3 bg-purple-400 rounded-full animate-pulse shadow-purple-400/50 shadow-lg"></div>
|
<div className="w-3 h-3 bg-purple-400 rounded-full animate-pulse shadow-purple-400/50 shadow-lg"></div>
|
||||||
@@ -69,7 +69,7 @@ export function ObjectivesBoard({
|
|||||||
|
|
||||||
{/* Bouton collapse séparé pour mobile */}
|
{/* Bouton collapse séparé pour mobile */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={toggleCollapse}
|
||||||
className="lg:hidden p-1 hover:bg-purple-900/20 rounded transition-colors"
|
className="lg:hidden p-1 hover:bg-purple-900/20 rounded transition-colors"
|
||||||
aria-label={isCollapsed ? "Développer" : "Réduire"}
|
aria-label={isCollapsed ? "Développer" : "Réduire"}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { TaskStatus } from '@/lib/types';
|
import { TaskStatus } from '@/lib/types';
|
||||||
|
import { userPreferencesService } from '@/services/user-preferences';
|
||||||
|
|
||||||
export function useColumnVisibility(initialHidden: TaskStatus[] = []) {
|
export function useColumnVisibility() {
|
||||||
const [hiddenStatuses, setHiddenStatuses] = useState<Set<TaskStatus>>(
|
const [hiddenStatuses, setHiddenStatuses] = useState<Set<TaskStatus>>(new Set());
|
||||||
new Set(initialHidden)
|
|
||||||
);
|
// Charger les préférences au montage
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = userPreferencesService.getColumnVisibility();
|
||||||
|
setHiddenStatuses(new Set(saved.hiddenStatuses));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleStatusVisibility = (status: TaskStatus) => {
|
const toggleStatusVisibility = (status: TaskStatus) => {
|
||||||
setHiddenStatuses(prev => {
|
setHiddenStatuses(prev => {
|
||||||
@@ -14,6 +19,12 @@ export function useColumnVisibility(initialHidden: TaskStatus[] = []) {
|
|||||||
} else {
|
} else {
|
||||||
newSet.add(status);
|
newSet.add(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sauvegarder dans localStorage
|
||||||
|
userPreferencesService.saveColumnVisibility({
|
||||||
|
hiddenStatuses: Array.from(newSet)
|
||||||
|
});
|
||||||
|
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
39
hooks/useObjectivesCollapse.ts
Normal file
39
hooks/useObjectivesCollapse.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
30
hooks/useObjectivesVisibility.ts
Normal file
30
hooks/useObjectivesVisibility.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
224
services/user-preferences.ts
Normal file
224
services/user-preferences.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { TaskPriority, TaskStatus } from '@/lib/types';
|
||||||
|
|
||||||
|
// Types pour les préférences utilisateur
|
||||||
|
export interface KanbanFilters {
|
||||||
|
search?: string;
|
||||||
|
tags?: string[];
|
||||||
|
priorities?: TaskPriority[];
|
||||||
|
showCompleted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewPreferences {
|
||||||
|
compactView: boolean;
|
||||||
|
swimlanesByTags: boolean;
|
||||||
|
showObjectives: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnVisibility {
|
||||||
|
hiddenStatuses: TaskStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserPreferences {
|
||||||
|
kanbanFilters: KanbanFilters;
|
||||||
|
viewPreferences: ViewPreferences;
|
||||||
|
columnVisibility: ColumnVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valeurs par défaut
|
||||||
|
const DEFAULT_PREFERENCES: UserPreferences = {
|
||||||
|
kanbanFilters: {
|
||||||
|
search: '',
|
||||||
|
tags: [],
|
||||||
|
priorities: [],
|
||||||
|
showCompleted: true
|
||||||
|
},
|
||||||
|
viewPreferences: {
|
||||||
|
compactView: false,
|
||||||
|
swimlanesByTags: false,
|
||||||
|
showObjectives: true
|
||||||
|
},
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
export const userPreferencesService = {
|
||||||
|
// === FILTRES KANBAN ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde les filtres Kanban
|
||||||
|
*/
|
||||||
|
saveKanbanFilters(filters: KanbanFilters): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.KANBAN_FILTERS, JSON.stringify(filters));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la sauvegarde des filtres Kanban:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les filtres Kanban
|
||||||
|
*/
|
||||||
|
getKanbanFilters(): KanbanFilters {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.KANBAN_FILTERS);
|
||||||
|
if (stored) {
|
||||||
|
return { ...DEFAULT_PREFERENCES.kanbanFilters, ...JSON.parse(stored) };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la récupération des filtres Kanban:', error);
|
||||||
|
}
|
||||||
|
return DEFAULT_PREFERENCES.kanbanFilters;
|
||||||
|
},
|
||||||
|
|
||||||
|
// === PRÉFÉRENCES DE VUE ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde les préférences de vue
|
||||||
|
*/
|
||||||
|
saveViewPreferences(preferences: ViewPreferences): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.VIEW_PREFERENCES, JSON.stringify(preferences));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la sauvegarde des préférences de vue:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les préférences de vue
|
||||||
|
*/
|
||||||
|
getViewPreferences(): ViewPreferences {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.VIEW_PREFERENCES);
|
||||||
|
if (stored) {
|
||||||
|
return { ...DEFAULT_PREFERENCES.viewPreferences, ...JSON.parse(stored) };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la récupération des préférences de vue:', error);
|
||||||
|
}
|
||||||
|
return DEFAULT_PREFERENCES.viewPreferences;
|
||||||
|
},
|
||||||
|
|
||||||
|
// === VISIBILITÉ DES COLONNES ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde la visibilité des colonnes
|
||||||
|
*/
|
||||||
|
saveColumnVisibility(visibility: ColumnVisibility): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.COLUMN_VISIBILITY, JSON.stringify(visibility));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la sauvegarde de la visibilité des colonnes:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la visibilité des colonnes
|
||||||
|
*/
|
||||||
|
getColumnVisibility(): ColumnVisibility {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.COLUMN_VISIBILITY);
|
||||||
|
if (stored) {
|
||||||
|
return { ...DEFAULT_PREFERENCES.columnVisibility, ...JSON.parse(stored) };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la récupération de la visibilité des colonnes:', error);
|
||||||
|
}
|
||||||
|
return DEFAULT_PREFERENCES.columnVisibility;
|
||||||
|
},
|
||||||
|
|
||||||
|
// === MÉTHODES GLOBALES ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les préférences utilisateur
|
||||||
|
*/
|
||||||
|
getAllPreferences(): UserPreferences {
|
||||||
|
return {
|
||||||
|
kanbanFilters: this.getKanbanFilters(),
|
||||||
|
viewPreferences: this.getViewPreferences(),
|
||||||
|
columnVisibility: this.getColumnVisibility()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde toutes les préférences utilisateur
|
||||||
|
*/
|
||||||
|
saveAllPreferences(preferences: UserPreferences): void {
|
||||||
|
this.saveKanbanFilters(preferences.kanbanFilters);
|
||||||
|
this.saveViewPreferences(preferences.viewPreferences);
|
||||||
|
this.saveColumnVisibility(preferences.columnVisibility);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remet à zéro toutes les préférences
|
||||||
|
*/
|
||||||
|
resetAllPreferences(): void {
|
||||||
|
try {
|
||||||
|
Object.values(STORAGE_KEYS).forEach(key => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la remise à zéro des préférences:', 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 });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour partiellement les préférences de vue
|
||||||
|
*/
|
||||||
|
updateViewPreferences(updates: Partial<ViewPreferences>): void {
|
||||||
|
const current = this.getViewPreferences();
|
||||||
|
this.saveViewPreferences({ ...current, ...updates });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la visibilité d'une colonne spécifique
|
||||||
|
*/
|
||||||
|
toggleColumnVisibility(status: TaskStatus): void {
|
||||||
|
const current = this.getColumnVisibility();
|
||||||
|
const hiddenStatuses = new Set(current.hiddenStatuses);
|
||||||
|
|
||||||
|
if (hiddenStatuses.has(status)) {
|
||||||
|
hiddenStatuses.delete(status);
|
||||||
|
} else {
|
||||||
|
hiddenStatuses.add(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveColumnVisibility({
|
||||||
|
hiddenStatuses: Array.from(hiddenStatuses)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, ReactNode, useState, useMemo } from 'react';
|
import { createContext, useContext, ReactNode, useState, useMemo, useEffect } from 'react';
|
||||||
import { useTasks } from '@/hooks/useTasks';
|
import { useTasks } from '@/hooks/useTasks';
|
||||||
import { useTags } from '@/hooks/useTags';
|
import { useTags } from '@/hooks/useTags';
|
||||||
|
import { userPreferencesService } from '@/services/user-preferences';
|
||||||
import { Task, Tag } from '@/lib/types';
|
import { Task, Tag } from '@/lib/types';
|
||||||
import { CreateTaskData, UpdateTaskData, TaskFilters } from '@/clients/tasks-client';
|
import { CreateTaskData, UpdateTaskData, TaskFilters } from '@/clients/tasks-client';
|
||||||
import { KanbanFilters } from '@/components/kanban/KanbanFilters';
|
import { KanbanFilters } from '@/components/kanban/KanbanFilters';
|
||||||
@@ -52,9 +53,44 @@ export function TasksProvider({ children, initialTasks, initialStats }: TasksPro
|
|||||||
|
|
||||||
const { tags, loading: tagsLoading, error: tagsError } = useTags();
|
const { tags, loading: tagsLoading, error: tagsError } = useTags();
|
||||||
|
|
||||||
// État des filtres Kanban
|
// État des filtres Kanban avec persistance
|
||||||
const [kanbanFilters, setKanbanFilters] = useState<KanbanFilters>({});
|
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,
|
||||||
|
compactView: savedViewPrefs.compactView,
|
||||||
|
swimlanesByTags: savedViewPrefs.swimlanesByTags
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fonction pour mettre à jour les filtres avec persistance
|
||||||
|
const updateKanbanFilters = (newFilters: KanbanFilters) => {
|
||||||
|
setKanbanFilters(newFilters);
|
||||||
|
|
||||||
|
// Sauvegarder les filtres
|
||||||
|
userPreferencesService.saveKanbanFilters({
|
||||||
|
search: newFilters.search,
|
||||||
|
tags: newFilters.tags,
|
||||||
|
priorities: newFilters.priorities,
|
||||||
|
showCompleted: newFilters.showCompleted
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sauvegarder les préférences de vue
|
||||||
|
userPreferencesService.saveViewPreferences({
|
||||||
|
compactView: newFilters.compactView || false,
|
||||||
|
swimlanesByTags: newFilters.swimlanesByTags || false,
|
||||||
|
showObjectives: true // Toujours visible pour l'instant
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Séparer les tâches épinglées (objectifs) des autres
|
// Séparer les tâches épinglées (objectifs) des autres
|
||||||
const { pinnedTasks, regularTasks } = useMemo(() => {
|
const { pinnedTasks, regularTasks } = useMemo(() => {
|
||||||
const pinnedTagNames = tags.filter(tag => tag.isPinned).map(tag => tag.name);
|
const pinnedTagNames = tags.filter(tag => tag.isPinned).map(tag => tag.name);
|
||||||
@@ -113,7 +149,7 @@ export function TasksProvider({ children, initialTasks, initialStats }: TasksPro
|
|||||||
tagsLoading,
|
tagsLoading,
|
||||||
tagsError,
|
tagsError,
|
||||||
kanbanFilters,
|
kanbanFilters,
|
||||||
setKanbanFilters,
|
setKanbanFilters: updateKanbanFilters,
|
||||||
filteredTasks,
|
filteredTasks,
|
||||||
pinnedTasks
|
pinnedTasks
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user