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:
Julien Froidefond
2025-09-14 22:42:22 +02:00
parent 1597f0fea1
commit da64221407
6 changed files with 352 additions and 12 deletions

View File

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

View File

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

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

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

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

View File

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