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.
This commit is contained in:
Julien Froidefond
2025-11-10 09:09:28 +01:00
parent 2d4c161e1d
commit c7c47039b4
8 changed files with 382 additions and 214 deletions

View File

@@ -37,6 +37,12 @@
--card-shadow-heavy: rgba(0, 0, 0, 0.25); --card-shadow-heavy: rgba(0, 0, 0, 0.25);
--card-glow-primary: rgba(8, 145, 178, 0.2); --card-glow-primary: rgba(8, 145, 178, 0.2);
--card-glow-accent: rgba(217, 119, 6, 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 { .light {
@@ -76,6 +82,12 @@
--card-shadow-heavy: rgba(0, 0, 0, 0.25); --card-shadow-heavy: rgba(0, 0, 0, 0.25);
--card-glow-primary: rgba(8, 145, 178, 0.2); --card-glow-primary: rgba(8, 145, 178, 0.2);
--card-glow-accent: rgba(217, 119, 6, 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 { .dark {

View File

@@ -2,7 +2,12 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { CheckSquare2, Calendar } from 'lucide-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 { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
@@ -13,6 +18,7 @@ import { StatusBadge } from '@/components/ui/StatusBadge';
import { ToggleButton } from '@/components/ui/ToggleButton'; import { ToggleButton } from '@/components/ui/ToggleButton';
import { DateTimeInput } from '@/components/ui/DateTimeInput'; import { DateTimeInput } from '@/components/ui/DateTimeInput';
import { tasksClient } from '@/clients/tasks-client'; import { tasksClient } from '@/clients/tasks-client';
import { getStatusConfig } from '@/lib/status-config';
interface EditCheckboxModalProps { interface EditCheckboxModalProps {
checkbox: DailyCheckbox; checkbox: DailyCheckbox;
@@ -72,20 +78,68 @@ export function EditCheckboxModal({
} }
}, [taskId, allTasks]); }, [taskId, allTasks]);
// Filtrer les tâches selon la recherche et exclure les tâches avec des tags "objectif principal" // Fonction pour identifier les statuts terminés
const filteredTasks = allTasks.filter((task) => { const isCompletedStatus = (status: string): boolean => {
return ['done', 'cancelled', 'archived'].includes(status);
};
// 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) // Exclure les tâches avec des tags marqués comme "objectif principal" (isPinned = true)
if (task.tagDetails && task.tagDetails.some((tag) => tag.isPinned)) { if (task.tagDetails && task.tagDetails.some((tag) => tag.isPinned)) {
return false; return false;
} }
// Filtrer selon la recherche // Filtrer selon la recherche
const searchLower = taskSearch.toLowerCase();
return ( return (
task.title.toLowerCase().includes(taskSearch.toLowerCase()) || task.title.toLowerCase().includes(searchLower) ||
(task.description && (task.description &&
task.description.toLowerCase().includes(taskSearch.toLowerCase())) 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) => { const handleTaskSelect = (task: Task) => {
setTaskId(task.id); setTaskId(task.id);
@@ -187,18 +241,18 @@ export function EditCheckboxModal({
{selectedTask ? ( {selectedTask ? (
// Tâche déjà sélectionnée // Tâche déjà sélectionnée
<Card className="p-3" background="muted"> <Card className="p-4" background="muted" shadow="sm">
<div className="flex items-center justify-between"> <div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate"> <div className="font-medium text-sm text-[var(--foreground)] truncate mb-1.5">
{selectedTask.title} {selectedTask.title}
</div> </div>
{selectedTask.description && ( {selectedTask.description && (
<div className="text-xs text-[var(--muted-foreground)] truncate"> <div className="text-xs text-[var(--muted-foreground)] truncate mb-2">
{selectedTask.description} {selectedTask.description}
</div> </div>
)} )}
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 flex-wrap">
<StatusBadge status={selectedTask.status} /> <StatusBadge status={selectedTask.status} />
{selectedTask.tags && selectedTask.tags.length > 0 && ( {selectedTask.tags && selectedTask.tags.length > 0 && (
<TagDisplay <TagDisplay
@@ -215,7 +269,7 @@ export function EditCheckboxModal({
onClick={() => setTaskId(undefined)} onClick={() => setTaskId(undefined)}
variant="ghost" variant="ghost"
size="sm" 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} disabled={saving}
> >
× ×
@@ -234,34 +288,64 @@ export function EditCheckboxModal({
/> />
{taskSearch.trim() && ( {taskSearch.trim() && (
<Card className="max-h-40 overflow-y-auto" shadow="sm"> <Card
className="max-h-64 overflow-y-auto"
shadow="sm"
background="default"
>
{tasksLoading ? ( {tasksLoading ? (
<div className="p-3 text-center text-sm text-[var(--muted-foreground)]"> <div className="p-4 text-center text-sm text-[var(--muted-foreground)]">
Chargement... Chargement...
</div> </div>
) : filteredTasks.length === 0 ? ( ) : filteredTasks.length === 0 ? (
<div className="p-3 text-center text-sm text-[var(--muted-foreground)]"> <div className="p-4 text-center text-sm text-[var(--muted-foreground)]">
Aucune tâche trouvée Aucune tâche trouvée
</div> </div>
) : ( ) : (
filteredTasks.slice(0, 5).map((task) => ( <div>
{filteredTasks.slice(0, 8).map((task, index) => {
const isCompleted = isCompletedStatus(task.status);
return (
<div key={task.id}>
{index > 0 && (
<div className="h-px bg-[color-mix(in_srgb,var(--border)_10%,transparent)]" />
)}
<button <button
key={task.id}
type="button" type="button"
onClick={() => handleTaskSelect(task)} onClick={() => handleTaskSelect(task)}
className="w-full text-left p-3 hover:bg-[var(--muted)]/50 transition-colors border-b border-[var(--border)]/30 last:border-b-0" className={`
w-full text-left p-3 transition-all duration-200
hover:bg-[color-mix(in_srgb,var(--primary)_8%,transparent)]
active:bg-[color-mix(in_srgb,var(--primary)_12%,transparent)]
disabled:opacity-50 disabled:cursor-not-allowed
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--primary)]/20 focus-visible:ring-inset
${isCompleted ? 'opacity-60' : 'opacity-100'}
`}
disabled={saving} disabled={saving}
> >
<div className="font-medium text-sm truncate"> <div
className={`
font-medium text-sm truncate
${isCompleted ? 'text-[var(--muted-foreground)]' : 'text-[var(--foreground)]'}
`}
>
{task.title} {task.title}
</div> </div>
{task.description && ( {task.description && (
<div className="text-xs text-[var(--muted-foreground)] truncate mt-1 max-w-full overflow-hidden"> <div
className={`
text-xs truncate mt-1.5 max-w-full overflow-hidden
${isCompleted ? 'text-[var(--muted-foreground)]/70' : 'text-[var(--muted-foreground)]'}
`}
>
{task.description} {task.description}
</div> </div>
)} )}
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-2 flex-wrap">
<StatusBadge status={task.status} /> <StatusBadge
status={task.status}
className={isCompleted ? 'opacity-70' : ''}
/>
{task.tags && task.tags.length > 0 && ( {task.tags && task.tags.length > 0 && (
<TagDisplay <TagDisplay
tags={task.tags} tags={task.tags}
@@ -272,7 +356,10 @@ export function EditCheckboxModal({
)} )}
</div> </div>
</button> </button>
)) </div>
);
})}
</div>
)} )}
</Card> </Card>
)} )}

View File

@@ -127,6 +127,7 @@ export function KanbanBoardContainer({
onEditTask={handleEditTask} onEditTask={handleEditTask}
onUpdateStatus={handleUpdateStatus} onUpdateStatus={handleUpdateStatus}
pinnedTagName={pinnedTagName} pinnedTagName={pinnedTagName}
visibleStatuses={visibleStatuses}
/> />
<BoardRouter <BoardRouter

View File

@@ -17,6 +17,7 @@ interface KanbanHeaderProps {
onEditTask: (task: Task) => void; onEditTask: (task: Task) => void;
onUpdateStatus: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus: (taskId: string, newStatus: TaskStatus) => Promise<void>;
pinnedTagName?: string; pinnedTagName?: string;
visibleStatuses?: TaskStatus[];
} }
export function KanbanHeader({ export function KanbanHeader({
@@ -30,6 +31,7 @@ export function KanbanHeader({
onEditTask, onEditTask,
onUpdateStatus, onUpdateStatus,
pinnedTagName, pinnedTagName,
visibleStatuses,
}: KanbanHeaderProps) { }: KanbanHeaderProps) {
return ( return (
<> <>
@@ -51,6 +53,7 @@ export function KanbanHeader({
onUpdateStatus={onUpdateStatus} onUpdateStatus={onUpdateStatus}
compactView={kanbanFilters.compactView as boolean} compactView={kanbanFilters.compactView as boolean}
pinnedTagName={pinnedTagName} pinnedTagName={pinnedTagName}
visibleStatuses={visibleStatuses}
/> />
)} )}
</> </>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { useDragAndDrop } from '@/hooks/useDragAndDrop'; import { useDragAndDrop } from '@/hooks/useDragAndDrop';
import { Task, TaskStatus } from '@/lib/types'; import { Task, TaskStatus } from '@/lib/types';
@@ -20,6 +20,8 @@ import {
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { Emoji } from '@/components/ui/Emoji'; import { Emoji } from '@/components/ui/Emoji';
import { getAllStatuses, getTechStyle } from '@/lib/status-config';
import { DropZone, ColumnHeader } from '@/components/ui';
interface ObjectivesBoardProps { interface ObjectivesBoardProps {
tasks: Task[]; tasks: Task[];
@@ -27,23 +29,18 @@ interface ObjectivesBoardProps {
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>; onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean; compactView?: boolean;
pinnedTagName?: string; pinnedTagName?: string;
visibleStatuses?: TaskStatus[];
} }
// Composant pour les colonnes droppables // Composant pour les colonnes droppables
function DroppableColumn({ function DroppableColumn({
status, status,
tasks, tasks,
title,
color,
icon,
onEditTask, onEditTask,
compactView, compactView,
}: { }: {
status: TaskStatus; status: TaskStatus;
tasks: Task[]; tasks: Task[];
title: string;
color: string;
icon: string;
onEditTask?: (task: Task) => void; onEditTask?: (task: Task) => void;
compactView: boolean; compactView: boolean;
}) { }) {
@@ -52,49 +49,23 @@ function DroppableColumn({
}); });
return ( return (
<div ref={setNodeRef} className="space-y-3"> <DropZone ref={setNodeRef} className="min-h-[100px] relative group/column">
<div className="flex items-center gap-2 pt-2 pb-2 border-b border-[var(--accent)]/20">
<div className={`w-2 h-2 rounded-full ${color}`}></div>
<h3
className={`text-sm font-mono font-medium uppercase tracking-wider ${color.replace('bg-', 'text-').replace('400', '300')}`}
>
{title}
</h3>
<div className="flex-1"></div>
<span className="text-xs text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
{tasks.length}
</span>
</div>
{tasks.length === 0 ? (
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm">
<div className="text-2xl mb-2">
<Emoji emoji={icon} />
</div>
Aucun objectif {title.toLowerCase()}
</div>
) : (
<SortableContext <SortableContext
items={tasks.map((t) => t.id)} items={tasks.map((t) => t.id)}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
<div className="space-y-3"> <div className="space-y-3">
{tasks.map((task) => ( {tasks.map((task) => (
<div
key={task.id}
className="transform hover:scale-[1.02] transition-transform duration-200"
>
<TaskCard <TaskCard
key={task.id}
task={task} task={task}
onEdit={onEditTask} onEdit={onEditTask}
compactView={compactView} compactView={compactView}
/> />
</div>
))} ))}
</div> </div>
</SortableContext> </SortableContext>
)} </DropZone>
</div>
); );
} }
@@ -104,11 +75,24 @@ export function ObjectivesBoard({
onUpdateStatus, onUpdateStatus,
compactView = false, compactView = false,
pinnedTagName = 'Objectifs', pinnedTagName = 'Objectifs',
visibleStatuses,
}: ObjectivesBoardProps) { }: ObjectivesBoardProps) {
const { preferences, toggleObjectivesCollapse } = useUserPreferences(); const { preferences, toggleObjectivesCollapse, isColumnVisible } =
const isCollapsed = preferences.viewPreferences.objectivesCollapsed; useUserPreferences();
const { isMounted, sensors } = useDragAndDrop(); const { isMounted, sensors } = useDragAndDrop();
const [activeTask, setActiveTask] = useState<Task | null>(null); const [activeTask, setActiveTask] = useState<Task | null>(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 // Handlers pour le drag & drop
const handleDragStart = (event: DragStartEvent) => { const handleDragStart = (event: DragStartEvent) => {
@@ -138,15 +122,14 @@ export function ObjectivesBoard({
const content = ( const content = (
<div className="bg-[var(--card)]/30 border-b border-[var(--accent)]/30"> <div className="bg-[var(--card)]/30 border-b border-[var(--accent)]/30">
<div className="container mx-auto px-6 py-4">
<Card <Card
variant="column" variant="column"
className="border-[var(--accent)]/30 shadow-[var(--accent)]/10" className="border-[var(--accent)]/30 shadow-[var(--accent)]/10"
> >
<CardHeader className="pb-4"> <CardHeader className="pb-4 px-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<button <button
onClick={toggleObjectivesCollapse} onClick={handleToggleCollapse}
className="flex items-center gap-3 hover:bg-[var(--accent)]/20 rounded-lg p-2 -m-2 transition-colors group" 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> <div className="w-3 h-3 bg-[var(--accent)] rounded-full animate-pulse shadow-[var(--accent)]/50 shadow-lg"></div>
@@ -164,17 +147,13 @@ export function ObjectivesBoard({
</button> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge <Badge variant="primary" size="sm" className="bg-[var(--accent)]">
variant="primary"
size="sm"
className="bg-[var(--accent)]"
>
{String(tasks.length).padStart(2, '0')} {String(tasks.length).padStart(2, '0')}
</Badge> </Badge>
{/* Bouton collapse séparé pour mobile */} {/* Bouton collapse séparé pour mobile */}
<button <button
onClick={toggleObjectivesCollapse} onClick={handleToggleCollapse}
className="lg:hidden p-1 hover:bg-[var(--accent)]/20 rounded transition-colors" className="lg:hidden p-1 hover:bg-[var(--accent)]/20 rounded transition-colors"
aria-label={isCollapsed ? 'Développer' : 'Réduire'} aria-label={isCollapsed ? 'Développer' : 'Réduire'}
> >
@@ -189,71 +168,97 @@ export function ObjectivesBoard({
</CardHeader> </CardHeader>
{!isCollapsed && ( {!isCollapsed && (
<CardContent className="pt-3"> <CardContent className="pt-3 px-0">
{(() => { {(() => {
// Séparer les tâches par statut // Obtenir toutes les colonnes configurées
const inProgressTasks = tasks.filter( const allColumns = getAllStatuses();
(task) => task.status === 'in_progress'
// 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<TaskStatus, Task[]>
); );
const todoTasks = tasks.filter(
(task) => task.status === 'todo' || task.status === 'backlog' // Mapper les colonnes visibles avec leurs tâches
); const columnsWithTasks = visibleColumns.map((colConfig) => {
const completedTasks = tasks.filter( return {
(task) => task.status === 'done' || task.status === 'archived' config: colConfig,
); tasks: tasksByStatus[colConfig.key] || [],
const frozenTasks = tasks.filter( };
(task) => task.status === 'freeze' });
if (columnsWithTasks.length === 0) {
return (
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm">
Aucune colonne visible
</div>
); );
}
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4"> <>
<DroppableColumn {/* Headers des colonnes */}
status="todo" <div
tasks={todoTasks} className={`grid gap-4 px-6 py-4`}
title="À faire" style={{
color="bg-[var(--primary)]" gridTemplateColumns: `repeat(${columnsWithTasks.length}, minmax(0, 1fr))`,
icon="📋" }}
onEditTask={onEditTask} >
compactView={compactView} {columnsWithTasks.map(({ config }) => {
/> const style = getTechStyle(config.color);
const tasksInStatus = tasksByStatus[config.key] || [];
<DroppableColumn return (
status="in_progress" <div key={config.key} className="text-center">
tasks={inProgressTasks} <ColumnHeader
title="En cours" title={config.label}
color="bg-yellow-400" icon={config.icon}
icon="🔄" count={tasksInStatus.length}
onEditTask={onEditTask} color={style.accent.replace('text-', '')}
compactView={compactView} accentColor={style.accent}
/> className="justify-center gap-4"
<DroppableColumn
status="freeze"
tasks={frozenTasks}
title="Gelé"
color="bg-purple-400"
icon="🧊"
onEditTask={onEditTask}
compactView={compactView}
/>
<DroppableColumn
status="done"
tasks={completedTasks}
title="Terminé"
color="bg-green-400"
icon="✅"
onEditTask={onEditTask}
compactView={compactView}
/> />
</div> </div>
); );
})}
</div>
{/* Colonnes avec les tâches */}
<div
className={`grid gap-4 px-6 pb-4`}
style={{
gridTemplateColumns: `repeat(${columnsWithTasks.length}, minmax(0, 1fr))`,
}}
>
{columnsWithTasks.map(({ config, tasks: columnTasks }) => {
return (
<DroppableColumn
key={config.key}
status={config.key}
tasks={columnTasks}
onEditTask={onEditTask}
compactView={compactView}
/>
);
})}
</div>
</>
);
})()} })()}
</CardContent> </CardContent>
)} )}
</Card> </Card>
</div> </div>
</div>
); );
if (!isMounted) { if (!isMounted) {

View File

@@ -1,22 +1,68 @@
'use client'; 'use client';
import { TaskStatus } from '@/lib/types'; import { TaskStatus } from '@/lib/types';
import { getStatusConfig, getStatusBadgeClasses } from '@/lib/status-config'; import { getStatusConfig, getStatusColor } from '@/lib/status-config';
interface StatusBadgeProps { interface StatusBadgeProps {
status: TaskStatus; status: TaskStatus;
className?: string; 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 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 ( return (
<span <span
className={`text-xs px-2 py-0.5 rounded font-medium border ${badgeClasses} ${className}`} className={`inline-flex items-center gap-1.5 rounded-full border font-medium transition-colors ${sizeClasses[size]} ${className}`}
style={{
backgroundColor: `color-mix(in srgb, ${colorVar} 10%, transparent)`,
borderColor: `color-mix(in srgb, ${colorVar} 25%, var(--border))`,
color: colorVar,
}}
> >
{config.icon} {config.label} {showDot && (
<div
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: colorVar }}
/>
)}
{showIcon && <span className="flex-shrink-0">{config.icon}</span>}
<span>{config.label}</span>
</span> </span>
); );
} }

View File

@@ -156,7 +156,7 @@ export function useDaily(
}); });
}); });
}, },
[dailyView] [dailyView, currentDate]
); );
const addYesterdayCheckbox = useCallback( const addYesterdayCheckbox = useCallback(
@@ -208,7 +208,7 @@ export function useDaily(
}); });
}); });
}, },
[dailyView] [dailyView, currentDate]
); );
const updateCheckbox = useCallback( const updateCheckbox = useCallback(

View File

@@ -206,6 +206,14 @@ export const PRIORITY_COLOR_MAP = {
red: '#f87171', // red-400 (urgent priority) red: '#f87171', // red-400 (urgent priority)
} as const; } 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 // Couleurs alternatives pour les graphiques et charts
export const PRIORITY_CHART_COLORS = { export const PRIORITY_CHART_COLORS = {
Faible: '#10b981', // green-500 (plus lisible dans les charts) 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]; 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 // Fonction pour récupérer la couleur d'un chart basée sur le label
export const getPriorityChartColor = (priorityLabel: string): string => { export const getPriorityChartColor = (priorityLabel: string): string => {
return ( return (