From c4d8bacd97bcaf005b1c3cd58fa7a3e9bad3a5a7 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 19 Sep 2025 13:34:15 +0200 Subject: [PATCH] feat: enhance GeneralSettingsPage with tag management - Added tag management functionality to the `GeneralSettingsPageClient`, including filtering, sorting, and CRUD operations for tags. - Integrated a modal for creating and editing tags, improving user experience in managing task labels. - Updated the `Header` component to replace the 'Tags' link with 'Manager'. - Removed the deprecated `TagsPage` and `TagsPageClient` components to streamline the codebase. - Adjusted data fetching in `page.tsx` to include initial tags alongside user preferences. --- .../settings/GeneralSettingsPageClient.tsx | 305 +++++++++++++++++- components/ui/Header.tsx | 1 - src/app/settings/general/page.tsx | 8 +- src/app/tags/TagsPageClient.tsx | 224 ------------- src/app/tags/page.tsx | 14 - 5 files changed, 307 insertions(+), 245 deletions(-) delete mode 100644 src/app/tags/TagsPageClient.tsx delete mode 100644 src/app/tags/page.tsx diff --git a/components/settings/GeneralSettingsPageClient.tsx b/components/settings/GeneralSettingsPageClient.tsx index 8490dae..b8d55a9 100644 --- a/components/settings/GeneralSettingsPageClient.tsx +++ b/components/settings/GeneralSettingsPageClient.tsx @@ -1,16 +1,85 @@ 'use client'; -import { UserPreferences } from '@/lib/types'; +import { useState, useMemo } from 'react'; +import { UserPreferences, Tag } from '@/lib/types'; +import { useTags } from '@/hooks/useTags'; import { Header } from '@/components/ui/Header'; -import { Card, CardContent } from '@/components/ui/Card'; +import { Card, CardContent, CardHeader } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { TagForm } from '@/components/forms/TagForm'; import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext'; import Link from 'next/link'; interface GeneralSettingsPageClientProps { initialPreferences: UserPreferences; + initialTags: Tag[]; } -export function GeneralSettingsPageClient({ initialPreferences }: GeneralSettingsPageClientProps) { +export function GeneralSettingsPageClient({ initialPreferences, initialTags }: GeneralSettingsPageClientProps) { + const { + tags, + refreshTags, + deleteTag + } = useTags(initialTags as (Tag & { usage: number })[]); + + const [searchQuery, setSearchQuery] = useState(''); + const [showOnlyUnused, setShowOnlyUnused] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [editingTag, setEditingTag] = useState(null); + const [deletingTagId, setDeletingTagId] = useState(null); + + // Filtrer et trier les tags + const filteredTags = useMemo(() => { + let filtered = tags; + + // Filtrer par recherche + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter(tag => + tag.name.toLowerCase().includes(query) + ); + } + + // Filtrer pour afficher seulement les non utilisés + if (showOnlyUnused) { + filtered = filtered.filter(tag => { + const usage = (tag as Tag & { usage?: number }).usage || 0; + return usage === 0; + }); + } + + const sorted = filtered.sort((a, b) => { + const usageA = (a as Tag & { usage?: number }).usage || 0; + const usageB = (b as Tag & { usage?: number }).usage || 0; + if (usageB !== usageA) return usageB - usageA; + return a.name.localeCompare(b.name); + }); + + // Limiter à 12 tags si pas de recherche ni filtre, sinon afficher tous les résultats + const hasFilters = searchQuery.trim() || showOnlyUnused; + return hasFilters ? sorted : sorted.slice(0, 12); + }, [tags, searchQuery, showOnlyUnused]); + + const handleEditTag = (tag: Tag) => { + setEditingTag(tag); + }; + + const handleDeleteTag = async (tag: Tag) => { + if (!confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) { + return; + } + + setDeletingTagId(tag.id); + try { + await deleteTag(tag.id); + await refreshTags(); + } catch (error) { + console.error('Erreur lors de la suppression:', error); + } finally { + setDeletingTagId(null); + } + }; return (
@@ -41,6 +110,210 @@ export function GeneralSettingsPageClient({ initialPreferences }: GeneralSetting
+ {/* Gestion des tags */} + + +
+
+

+ 🏷️ Gestion des tags +

+

+ Créer et organiser les étiquettes pour vos tâches +

+
+ +
+
+ + {/* Stats des tags */} +
+
+
{tags.length}
+
Tags créés
+
+
+
+ {tags.reduce((sum, tag) => sum + ((tag as Tag & { usage?: number }).usage || 0), 0)} +
+
Utilisations
+
+
+
+ {tags.filter(tag => (tag as Tag & { usage?: number }).usage && (tag as Tag & { usage?: number }).usage! > 0).length} +
+
Actifs
+
+
+ + {/* Recherche et filtres */} +
+ setSearchQuery(e.target.value)} + className="w-full" + /> + + {/* Filtres rapides */} +
+ + + {(searchQuery || showOnlyUnused) && ( + + )} +
+
+ + {/* Liste des tags en grid */} + {filteredTags.length === 0 ? ( +
+ {searchQuery && showOnlyUnused ? 'Aucun tag non utilisé trouvé avec cette recherche' : + searchQuery ? 'Aucun tag trouvé pour cette recherche' : + showOnlyUnused ? '🎉 Aucun tag non utilisé ! Tous vos tags sont actifs.' : + 'Aucun tag créé'} + {!searchQuery && !showOnlyUnused && ( +
+ +
+ )} +
+ ) : ( +
+ {/* Grid des tags */} +
+ {filteredTags.map((tag) => { + const usage = (tag as Tag & { usage?: number }).usage || 0; + const isUnused = usage === 0; + return ( +
+ {/* Header du tag */} +
+
+
+ {tag.name} + {tag.isPinned && ( + + 📌 + + )} +
+ + {/* Actions */} +
+ + +
+
+ + {/* Stats et warning */} +
+
+ {usage} utilisation{usage !== 1 ? 's' : ''} + {isUnused && ( + + ⚠️ Non utilisé + + )} +
+ {('createdAt' in tag && (tag as Tag & { createdAt: Date }).createdAt) && ( +
+ Créé le {new Date((tag as Tag & { createdAt: Date }).createdAt).toLocaleDateString('fr-FR')} +
+ )} +
+
+ ); + })} +
+ + {/* Message si plus de tags */} + {tags.length > 12 && !searchQuery && !showOnlyUnused && ( +
+ Et {tags.length - 12} autres tags... (utilisez la recherche ou les filtres pour les voir) +
+ )} +
+ )} + + + {/* Note développement futur */} @@ -49,7 +322,7 @@ export function GeneralSettingsPageClient({ initialPreferences }: GeneralSetting 🚧 Interface de configuration en développement

- Les contrôles interactifs pour modifier ces préférences seront disponibles dans une prochaine version. + Les contrôles interactifs pour modifier les autres préférences seront disponibles dans une prochaine version. Pour l'instant, les préférences sont modifiables via les boutons de l'interface principale.

@@ -59,6 +332,30 @@ export function GeneralSettingsPageClient({ initialPreferences }: GeneralSetting
+ + {/* Modals pour les tags */} + {isCreateModalOpen && ( + setIsCreateModalOpen(false)} + onSuccess={async () => { + setIsCreateModalOpen(false); + await refreshTags(); + }} + /> + )} + + {editingTag && ( + setEditingTag(null)} + onSuccess={async () => { + setEditingTag(null); + await refreshTags(); + }} + /> + )}
); } diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx index a348a01..6a5af58 100644 --- a/components/ui/Header.tsx +++ b/components/ui/Header.tsx @@ -54,7 +54,6 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s { href: '/kanban', label: 'Kanban' }, { href: '/daily', label: 'Daily' }, { href: '/weekly-summary', label: 'Hebdo' }, - { href: '/tags', label: 'Tags' }, ...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []), { href: '/settings', label: 'Settings' } ]; diff --git a/src/app/settings/general/page.tsx b/src/app/settings/general/page.tsx index 589c6f0..2594b75 100644 --- a/src/app/settings/general/page.tsx +++ b/src/app/settings/general/page.tsx @@ -1,4 +1,5 @@ import { userPreferencesService } from '@/services/user-preferences'; +import { tagsService } from '@/services/tags'; import { GeneralSettingsPageClient } from '@/components/settings/GeneralSettingsPageClient'; // Force dynamic rendering for real-time data @@ -6,7 +7,10 @@ export const dynamic = 'force-dynamic'; export default async function GeneralSettingsPage() { // Fetch data server-side - const preferences = await userPreferencesService.getAllPreferences(); + const [preferences, tags] = await Promise.all([ + userPreferencesService.getAllPreferences(), + tagsService.getTags() + ]); - return ; + return ; } diff --git a/src/app/tags/TagsPageClient.tsx b/src/app/tags/TagsPageClient.tsx deleted file mode 100644 index faa9701..0000000 --- a/src/app/tags/TagsPageClient.tsx +++ /dev/null @@ -1,224 +0,0 @@ -'use client'; - -import { useState, useMemo } from 'react'; -import React from 'react'; -import { Tag } from '@/lib/types'; -import { useTags } from '@/hooks/useTags'; -import { Button } from '@/components/ui/Button'; -import { Input } from '@/components/ui/Input'; -import { TagForm } from '@/components/forms/TagForm'; -import { Header } from '@/components/ui/Header'; - -interface TagsPageClientProps { - initialTags: Tag[]; -} - -export function TagsPageClient({ initialTags }: TagsPageClientProps) { - const { - tags, - loading, - error, - refreshTags, - deleteTag - } = useTags(initialTags as (Tag & { usage: number })[]); - - const [searchQuery, setSearchQuery] = useState(''); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const [editingTag, setEditingTag] = useState(null); - const [deletingTagId, setDeletingTagId] = useState(null); - - // Filtrer et trier les tags - const filteredAndSortedTags = useMemo(() => { - let filtered = tags; - - // Filtrer par recherche - if (searchQuery.trim()) { - const query = searchQuery.toLowerCase(); - filtered = tags.filter(tag => - tag.name.toLowerCase().includes(query) - ); - } - - // Trier par usage puis par nom - return filtered.sort((a, b) => { - const usageA = (a as Tag & { usage?: number }).usage || 0; - const usageB = (b as Tag & { usage?: number }).usage || 0; - if (usageB !== usageA) return usageB - usageA; - return a.name.localeCompare(b.name); - }); - }, [tags, searchQuery]); - - const handleEditTag = (tag: Tag) => { - setEditingTag(tag); - }; - - const handleDeleteTag = async (tag: Tag) => { - if (!confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) { - return; - } - - setDeletingTagId(tag.id); - try { - // Utiliser la server action directement depuis useTags - await deleteTag(tag.id); - // Refresh la liste des tags - await refreshTags(); - } catch (error) { - console.error('Erreur lors de la suppression:', error); - } finally { - setDeletingTagId(null); - } - }; - - return ( -
- {/* Header uniforme */} -
- - {/* Header spécifique aux tags */} -
-
-
-
-

- Tags ({filteredAndSortedTags.length}) -

-
- - -
-
-
- - {/* Contenu principal */} -
- {/* Barre de recherche */} -
- setSearchQuery(e.target.value)} - placeholder="Rechercher un tag..." - className="w-full" - /> -
- - {/* Messages d'état */} - {error && ( -
-
Erreur : {error}
-
- )} - - {loading && ( -
-
Chargement...
-
- )} - - {/* Tags en grille compacte */} - {!loading && ( -
- {filteredAndSortedTags.length === 0 ? ( -
-
🏷️
-

- {searchQuery ? 'Aucun tag trouvé' : 'Aucun tag'} -

-

- {searchQuery ? 'Essayez un autre terme' : 'Créez votre premier tag'} -

-
- ) : ( -
- {filteredAndSortedTags.map((tag) => { - const isDeleting = deletingTagId === tag.id; - const usage = (tag as Tag & { usage?: number }).usage || 0; - - return ( -
- {/* Actions en overlay */} -
- - -
- - {/* Contenu principal */} -
-
-
-

- {tag.name} -

-

- {usage} tâche{usage > 1 ? 's' : ''} -

- {tag.isPinned && ( - - 🎯 Objectif - - )} -
-
-
- ); - })} -
- )} -
- )} -
- - {/* Modals */} - setIsCreateModalOpen(false)} - onSuccess={refreshTags} - /> - - setEditingTag(null)} - onSuccess={refreshTags} - tag={editingTag} - /> -
- ); -} diff --git a/src/app/tags/page.tsx b/src/app/tags/page.tsx deleted file mode 100644 index 77b269e..0000000 --- a/src/app/tags/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { tagsService } from '@/services/tags'; -import { TagsPageClient } from './TagsPageClient'; - -// Force dynamic rendering (no static generation) -export const dynamic = 'force-dynamic'; - -export default async function TagsPage() { - // SSR - Récupération des tags côté serveur - const initialTags = await tagsService.getTags(); - - return ( - - ); -}