From c7c47039b40b25d78565bc1b1772a9124c9c5d56 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 10 Nov 2025 09:09:28 +0100 Subject: [PATCH] feat(EditCheckboxModal, ObjectivesBoard, StatusBadge): enhance task filtering and status handling - Improved task filtering in EditCheckboxModal to prioritize non-completed tasks and enhance relevance scoring. - Updated ObjectivesBoard to support dynamic visibility of task statuses and improved layout for better user experience. - Enhanced StatusBadge component to support size variations and customizable display options for task statuses. - Added new CSS variables for task priority colors in globals.css to standardize priority indicators across the application. --- src/app/globals.css | 12 + src/components/daily/EditCheckboxModal.tsx | 189 ++++++++---- src/components/kanban/BoardContainer.tsx | 1 + src/components/kanban/KanbanHeader.tsx | 3 + src/components/kanban/ObjectivesBoard.tsx | 317 +++++++++++---------- src/components/ui/StatusBadge.tsx | 56 +++- src/hooks/useDaily.ts | 4 +- src/lib/status-config.ts | 14 + 8 files changed, 382 insertions(+), 214 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 7d76a30..b97137a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -37,6 +37,12 @@ --card-shadow-heavy: rgba(0, 0, 0, 0.25); --card-glow-primary: rgba(8, 145, 178, 0.2); --card-glow-accent: rgba(217, 119, 6, 0.2); + + /* Couleurs de priorité pour les pastilles */ + --priority-blue: #60a5fa; /* blue-400 (low priority) */ + --priority-yellow: #fbbf24; /* amber-400 (medium priority) */ + --priority-purple: #a78bfa; /* violet-400 (high priority) */ + --priority-red: #f87171; /* red-400 (urgent priority) */ } .light { @@ -76,6 +82,12 @@ --card-shadow-heavy: rgba(0, 0, 0, 0.25); --card-glow-primary: rgba(8, 145, 178, 0.2); --card-glow-accent: rgba(217, 119, 6, 0.2); + + /* Couleurs de priorité pour les pastilles */ + --priority-blue: #60a5fa; /* blue-400 (low priority) */ + --priority-yellow: #fbbf24; /* amber-400 (medium priority) */ + --priority-purple: #a78bfa; /* violet-400 (high priority) */ + --priority-red: #f87171; /* red-400 (urgent priority) */ } .dark { diff --git a/src/components/daily/EditCheckboxModal.tsx b/src/components/daily/EditCheckboxModal.tsx index 3cafd08..1d5ddf7 100644 --- a/src/components/daily/EditCheckboxModal.tsx +++ b/src/components/daily/EditCheckboxModal.tsx @@ -2,7 +2,12 @@ import { useState, useEffect } from 'react'; import { CheckSquare2, Calendar } from 'lucide-react'; -import { DailyCheckbox, DailyCheckboxType, Task } from '@/lib/types'; +import { + DailyCheckbox, + DailyCheckboxType, + Task, + TaskStatus, +} from '@/lib/types'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Modal } from '@/components/ui/Modal'; @@ -13,6 +18,7 @@ import { StatusBadge } from '@/components/ui/StatusBadge'; import { ToggleButton } from '@/components/ui/ToggleButton'; import { DateTimeInput } from '@/components/ui/DateTimeInput'; import { tasksClient } from '@/clients/tasks-client'; +import { getStatusConfig } from '@/lib/status-config'; interface EditCheckboxModalProps { checkbox: DailyCheckbox; @@ -72,20 +78,68 @@ export function EditCheckboxModal({ } }, [taskId, allTasks]); - // Filtrer les tâches selon la recherche et exclure les tâches avec des tags "objectif principal" - const filteredTasks = allTasks.filter((task) => { - // Exclure les tâches avec des tags marqués comme "objectif principal" (isPinned = true) - if (task.tagDetails && task.tagDetails.some((tag) => tag.isPinned)) { - return false; - } + // Fonction pour identifier les statuts terminés + const isCompletedStatus = (status: string): boolean => { + return ['done', 'cancelled', 'archived'].includes(status); + }; - // Filtrer selon la recherche - return ( - task.title.toLowerCase().includes(taskSearch.toLowerCase()) || - (task.description && - task.description.toLowerCase().includes(taskSearch.toLowerCase())) - ); - }); + // Filtrer et trier les tâches selon la recherche et exclure les tâches avec des tags "objectif principal" + const filteredTasks = allTasks + .filter((task) => { + // Exclure les tâches avec des tags marqués comme "objectif principal" (isPinned = true) + if (task.tagDetails && task.tagDetails.some((tag) => tag.isPinned)) { + return false; + } + + // Filtrer selon la recherche + const searchLower = taskSearch.toLowerCase(); + return ( + task.title.toLowerCase().includes(searchLower) || + (task.description && + task.description.toLowerCase().includes(searchLower)) + ); + }) + .map((task) => { + // Calculer la pertinence de la recherche pour chaque tâche + const searchLower = taskSearch.toLowerCase(); + const titleMatch = task.title.toLowerCase().includes(searchLower); + const descriptionMatch = + task.description?.toLowerCase().includes(searchLower) || false; + + return { + task, + // Score de pertinence : match dans titre = 2, match dans description = 1 + relevanceScore: titleMatch ? 2 : descriptionMatch ? 1 : 0, + }; + }) + .sort((a, b) => { + const { task: taskA, relevanceScore: relevanceA } = a; + const { task: taskB, relevanceScore: relevanceB } = b; + + // 1. Prioriser les tâches non terminées (même si elles apparaissent plus tard dans la recherche) + const aCompleted = isCompletedStatus(taskA.status); + const bCompleted = isCompletedStatus(taskB.status); + + if (aCompleted && !bCompleted) return 1; + if (!aCompleted && bCompleted) return -1; + + // 2. Si même statut de complétion, trier par ordre de statut (in_progress avant todo, etc.) + if (!aCompleted && !bCompleted) { + const statusA = getStatusConfig(taskA.status as TaskStatus); + const statusB = getStatusConfig(taskB.status as TaskStatus); + const statusDiff = statusB.order - statusA.order; // Ordre décroissant (in_progress=2 avant todo=1) + if (statusDiff !== 0) return statusDiff; + } + + // 3. Prioriser les matches dans le titre vs la description + if (relevanceA !== relevanceB) { + return relevanceB - relevanceA; // Score décroissant + } + + // 4. Enfin, trier par titre alphabétique + return taskA.title.localeCompare(taskB.title); + }) + .map(({ task }) => task); // Extraire les tâches du mapping const handleTaskSelect = (task: Task) => { setTaskId(task.id); @@ -187,18 +241,18 @@ export function EditCheckboxModal({ {selectedTask ? ( // Tâche déjà sélectionnée - -
+ +
-
+
{selectedTask.title}
{selectedTask.description && ( -
+
{selectedTask.description}
)} -
+
{selectedTask.tags && selectedTask.tags.length > 0 && ( setTaskId(undefined)} variant="ghost" size="sm" - className="text-[var(--destructive)] hover:bg-[var(--destructive)]/10" + className="text-[var(--destructive)] hover:bg-[color-mix(in_srgb,var(--destructive)_10%,transparent)] flex-shrink-0" disabled={saving} > × @@ -234,45 +288,78 @@ export function EditCheckboxModal({ /> {taskSearch.trim() && ( - + {tasksLoading ? ( -
+
Chargement...
) : filteredTasks.length === 0 ? ( -
+
Aucune tâche trouvée
) : ( - filteredTasks.slice(0, 5).map((task) => ( -
- )} -
- - {task.tags && task.tags.length > 0 && ( - - )} -
- - )) + ); + })} +
)}
)} diff --git a/src/components/kanban/BoardContainer.tsx b/src/components/kanban/BoardContainer.tsx index c01c01e..d1756ac 100644 --- a/src/components/kanban/BoardContainer.tsx +++ b/src/components/kanban/BoardContainer.tsx @@ -127,6 +127,7 @@ export function KanbanBoardContainer({ onEditTask={handleEditTask} onUpdateStatus={handleUpdateStatus} pinnedTagName={pinnedTagName} + visibleStatuses={visibleStatuses} /> void; onUpdateStatus: (taskId: string, newStatus: TaskStatus) => Promise; pinnedTagName?: string; + visibleStatuses?: TaskStatus[]; } export function KanbanHeader({ @@ -30,6 +31,7 @@ export function KanbanHeader({ onEditTask, onUpdateStatus, pinnedTagName, + visibleStatuses, }: KanbanHeaderProps) { return ( <> @@ -51,6 +53,7 @@ export function KanbanHeader({ onUpdateStatus={onUpdateStatus} compactView={kanbanFilters.compactView as boolean} pinnedTagName={pinnedTagName} + visibleStatuses={visibleStatuses} /> )} diff --git a/src/components/kanban/ObjectivesBoard.tsx b/src/components/kanban/ObjectivesBoard.tsx index 8bf550e..fdf0beb 100644 --- a/src/components/kanban/ObjectivesBoard.tsx +++ b/src/components/kanban/ObjectivesBoard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useDragAndDrop } from '@/hooks/useDragAndDrop'; import { Task, TaskStatus } from '@/lib/types'; @@ -20,6 +20,8 @@ import { } from '@dnd-kit/sortable'; import { useDroppable } from '@dnd-kit/core'; import { Emoji } from '@/components/ui/Emoji'; +import { getAllStatuses, getTechStyle } from '@/lib/status-config'; +import { DropZone, ColumnHeader } from '@/components/ui'; interface ObjectivesBoardProps { tasks: Task[]; @@ -27,23 +29,18 @@ interface ObjectivesBoardProps { onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise; compactView?: boolean; pinnedTagName?: string; + visibleStatuses?: TaskStatus[]; } // Composant pour les colonnes droppables function DroppableColumn({ status, tasks, - title, - color, - icon, onEditTask, compactView, }: { status: TaskStatus; tasks: Task[]; - title: string; - color: string; - icon: string; onEditTask?: (task: Task) => void; compactView: boolean; }) { @@ -52,49 +49,23 @@ function DroppableColumn({ }); return ( -
-
-
-

- {title} -

-
- - {tasks.length} - -
- - {tasks.length === 0 ? ( -
-
- -
- Aucun objectif {title.toLowerCase()} + + t.id)} + strategy={verticalListSortingStrategy} + > +
+ {tasks.map((task) => ( + + ))}
- ) : ( - t.id)} - strategy={verticalListSortingStrategy} - > -
- {tasks.map((task) => ( -
- -
- ))} -
-
- )} -
+ + ); } @@ -104,11 +75,24 @@ export function ObjectivesBoard({ onUpdateStatus, compactView = false, pinnedTagName = 'Objectifs', + visibleStatuses, }: ObjectivesBoardProps) { - const { preferences, toggleObjectivesCollapse } = useUserPreferences(); - const isCollapsed = preferences.viewPreferences.objectivesCollapsed; + const { preferences, toggleObjectivesCollapse, isColumnVisible } = + useUserPreferences(); const { isMounted, sensors } = useDragAndDrop(); const [activeTask, setActiveTask] = useState(null); + const [isCollapsed, setIsCollapsed] = useState(true); // Initialiser à true pour éviter l'hydratation mismatch + + // Synchroniser avec les préférences après le montage + useEffect(() => { + setIsCollapsed(preferences.viewPreferences.objectivesCollapsed); + }, [preferences.viewPreferences.objectivesCollapsed]); + + // Handler pour toggle qui met à jour les préférences ET l'état local + const handleToggleCollapse = () => { + toggleObjectivesCollapse(); + setIsCollapsed(!isCollapsed); + }; // Handlers pour le drag & drop const handleDragStart = (event: DragStartEvent) => { @@ -138,121 +122,142 @@ export function ObjectivesBoard({ const content = (
-
- - -
- - -
+ + +
+ - {/* Bouton collapse séparé pour mobile */} - -
+
+ + {String(tasks.length).padStart(2, '0')} + + + {/* Bouton collapse séparé pour mobile */} +
-
+
+ - {!isCollapsed && ( - - {(() => { - // Séparer les tâches par statut - const inProgressTasks = tasks.filter( - (task) => task.status === 'in_progress' - ); - const todoTasks = tasks.filter( - (task) => task.status === 'todo' || task.status === 'backlog' - ); - const completedTasks = tasks.filter( - (task) => task.status === 'done' || task.status === 'archived' - ); - const frozenTasks = tasks.filter( - (task) => task.status === 'freeze' - ); + {!isCollapsed && ( + + {(() => { + // Obtenir toutes les colonnes configurées + const allColumns = getAllStatuses(); + // Filtrer les colonnes visibles selon le filtrage global + const visibleColumns = visibleStatuses + ? allColumns.filter((col) => visibleStatuses.includes(col.key)) + : allColumns.filter((col) => isColumnVisible(col.key)); + + // Organiser les tâches par statut (comme dans le Kanban principal) + const tasksByStatus = tasks.reduce( + (acc, task) => { + if (!acc[task.status]) { + acc[task.status] = []; + } + acc[task.status].push(task); + return acc; + }, + {} as Record + ); + + // Mapper les colonnes visibles avec leurs tâches + const columnsWithTasks = visibleColumns.map((colConfig) => { + return { + config: colConfig, + tasks: tasksByStatus[colConfig.key] || [], + }; + }); + + if (columnsWithTasks.length === 0) { return ( -
- - - - - - - +
+ Aucune colonne visible
); - })()} - - )} - -
+ } + + return ( + <> + {/* Headers des colonnes */} +
+ {columnsWithTasks.map(({ config }) => { + const style = getTechStyle(config.color); + const tasksInStatus = tasksByStatus[config.key] || []; + return ( +
+ +
+ ); + })} +
+ + {/* Colonnes avec les tâches */} +
+ {columnsWithTasks.map(({ config, tasks: columnTasks }) => { + return ( + + ); + })} +
+ + ); + })()} +
+ )} +
); diff --git a/src/components/ui/StatusBadge.tsx b/src/components/ui/StatusBadge.tsx index b15c440..26f0741 100644 --- a/src/components/ui/StatusBadge.tsx +++ b/src/components/ui/StatusBadge.tsx @@ -1,22 +1,68 @@ 'use client'; import { TaskStatus } from '@/lib/types'; -import { getStatusConfig, getStatusBadgeClasses } from '@/lib/status-config'; +import { getStatusConfig, getStatusColor } from '@/lib/status-config'; interface StatusBadgeProps { status: TaskStatus; className?: string; + size?: 'sm' | 'md' | 'lg'; + showDot?: boolean; + showIcon?: boolean; } -export function StatusBadge({ status, className = '' }: StatusBadgeProps) { +export function StatusBadge({ + status, + className = '', + size = 'sm', + showDot = true, + showIcon = true, +}: StatusBadgeProps) { const config = getStatusConfig(status); - const badgeClasses = getStatusBadgeClasses(status); + const colorKey = getStatusColor(status); + + // Mapper les couleurs de statut aux variables CSS du design system + const getStatusColorVar = (color: string): string => { + switch (color) { + case 'blue': + return 'var(--primary)'; + case 'green': + return 'var(--success)'; + case 'red': + return 'var(--destructive)'; + case 'purple': + return 'var(--purple)'; + case 'gray': + default: + return 'var(--gray)'; + } + }; + + const colorVar = getStatusColorVar(colorKey); + + const sizeClasses = { + sm: 'text-xs px-2 py-0.5', + md: 'text-sm px-2 py-1', + lg: 'text-base px-3 py-1.5', + }; return ( - {config.icon} {config.label} + {showDot && ( +
+ )} + {showIcon && {config.icon}} + {config.label} ); } diff --git a/src/hooks/useDaily.ts b/src/hooks/useDaily.ts index c3e98d6..86692f3 100644 --- a/src/hooks/useDaily.ts +++ b/src/hooks/useDaily.ts @@ -156,7 +156,7 @@ export function useDaily( }); }); }, - [dailyView] + [dailyView, currentDate] ); const addYesterdayCheckbox = useCallback( @@ -208,7 +208,7 @@ export function useDaily( }); }); }, - [dailyView] + [dailyView, currentDate] ); const updateCheckbox = useCallback( diff --git a/src/lib/status-config.ts b/src/lib/status-config.ts index d8c0a32..f2075f0 100644 --- a/src/lib/status-config.ts +++ b/src/lib/status-config.ts @@ -206,6 +206,14 @@ export const PRIORITY_COLOR_MAP = { red: '#f87171', // red-400 (urgent priority) } as const; +// CSS Variables pour les priorités (pour éviter les problèmes d'hydratation) +export const PRIORITY_CSS_VAR_MAP = { + blue: 'var(--priority-blue)', + yellow: 'var(--priority-yellow)', + purple: 'var(--priority-purple)', + red: 'var(--priority-red)', +} as const; + // Couleurs alternatives pour les graphiques et charts export const PRIORITY_CHART_COLORS = { Faible: '#10b981', // green-500 (plus lisible dans les charts) @@ -219,6 +227,12 @@ export const getPriorityColorHex = (color: PriorityConfig['color']): string => { return PRIORITY_COLOR_MAP[color]; }; +export const getPriorityColorCSSVar = ( + color: PriorityConfig['color'] +): string => { + return PRIORITY_CSS_VAR_MAP[color]; +}; + // Fonction pour récupérer la couleur d'un chart basée sur le label export const getPriorityChartColor = (priorityLabel: string): string => { return (