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:
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
// Fonction pour identifier les statuts terminés
|
||||
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)
|
||||
if (task.tagDetails && task.tagDetails.some((tag) => tag.isPinned)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrer selon la recherche
|
||||
const searchLower = taskSearch.toLowerCase();
|
||||
return (
|
||||
task.title.toLowerCase().includes(taskSearch.toLowerCase()) ||
|
||||
task.title.toLowerCase().includes(searchLower) ||
|
||||
(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) => {
|
||||
setTaskId(task.id);
|
||||
@@ -187,18 +241,18 @@ export function EditCheckboxModal({
|
||||
|
||||
{selectedTask ? (
|
||||
// Tâche déjà sélectionnée
|
||||
<Card className="p-3" background="muted">
|
||||
<div className="flex items-center justify-between">
|
||||
<Card className="p-4" background="muted" shadow="sm">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<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}
|
||||
</div>
|
||||
{selectedTask.description && (
|
||||
<div className="text-xs text-[var(--muted-foreground)] truncate">
|
||||
<div className="text-xs text-[var(--muted-foreground)] truncate mb-2">
|
||||
{selectedTask.description}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<StatusBadge status={selectedTask.status} />
|
||||
{selectedTask.tags && selectedTask.tags.length > 0 && (
|
||||
<TagDisplay
|
||||
@@ -215,7 +269,7 @@ export function EditCheckboxModal({
|
||||
onClick={() => 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,34 +288,64 @@ export function EditCheckboxModal({
|
||||
/>
|
||||
|
||||
{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 ? (
|
||||
<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...
|
||||
</div>
|
||||
) : 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
|
||||
</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
|
||||
key={task.id}
|
||||
type="button"
|
||||
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}
|
||||
>
|
||||
<div className="font-medium text-sm truncate">
|
||||
<div
|
||||
className={`
|
||||
font-medium text-sm truncate
|
||||
${isCompleted ? 'text-[var(--muted-foreground)]' : 'text-[var(--foreground)]'}
|
||||
`}
|
||||
>
|
||||
{task.title}
|
||||
</div>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<StatusBadge status={task.status} />
|
||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||
<StatusBadge
|
||||
status={task.status}
|
||||
className={isCompleted ? 'opacity-70' : ''}
|
||||
/>
|
||||
{task.tags && task.tags.length > 0 && (
|
||||
<TagDisplay
|
||||
tags={task.tags}
|
||||
@@ -272,7 +356,10 @@ export function EditCheckboxModal({
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -127,6 +127,7 @@ export function KanbanBoardContainer({
|
||||
onEditTask={handleEditTask}
|
||||
onUpdateStatus={handleUpdateStatus}
|
||||
pinnedTagName={pinnedTagName}
|
||||
visibleStatuses={visibleStatuses}
|
||||
/>
|
||||
|
||||
<BoardRouter
|
||||
|
||||
@@ -17,6 +17,7 @@ interface KanbanHeaderProps {
|
||||
onEditTask: (task: Task) => void;
|
||||
onUpdateStatus: (taskId: string, newStatus: TaskStatus) => Promise<void>;
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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<void>;
|
||||
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 (
|
||||
<div ref={setNodeRef} className="space-y-3">
|
||||
<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>
|
||||
) : (
|
||||
<DropZone ref={setNodeRef} className="min-h-[100px] relative group/column">
|
||||
<SortableContext
|
||||
items={tasks.map((t) => t.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="transform hover:scale-[1.02] transition-transform duration-200"
|
||||
>
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
onEdit={onEditTask}
|
||||
compactView={compactView}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
)}
|
||||
</div>
|
||||
</DropZone>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
@@ -138,15 +122,14 @@ export function ObjectivesBoard({
|
||||
|
||||
const content = (
|
||||
<div className="bg-[var(--card)]/30 border-b border-[var(--accent)]/30">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<Card
|
||||
variant="column"
|
||||
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">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="bg-[var(--accent)]"
|
||||
>
|
||||
<Badge variant="primary" size="sm" className="bg-[var(--accent)]">
|
||||
{String(tasks.length).padStart(2, '0')}
|
||||
</Badge>
|
||||
|
||||
{/* Bouton collapse séparé pour mobile */}
|
||||
<button
|
||||
onClick={toggleObjectivesCollapse}
|
||||
onClick={handleToggleCollapse}
|
||||
className="lg:hidden p-1 hover:bg-[var(--accent)]/20 rounded transition-colors"
|
||||
aria-label={isCollapsed ? 'Développer' : 'Réduire'}
|
||||
>
|
||||
@@ -189,71 +168,97 @@ export function ObjectivesBoard({
|
||||
</CardHeader>
|
||||
|
||||
{!isCollapsed && (
|
||||
<CardContent className="pt-3">
|
||||
<CardContent className="pt-3 px-0">
|
||||
{(() => {
|
||||
// Séparer les tâches par statut
|
||||
const inProgressTasks = tasks.filter(
|
||||
(task) => task.status === 'in_progress'
|
||||
// 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<TaskStatus, Task[]>
|
||||
);
|
||||
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'
|
||||
|
||||
// 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 (
|
||||
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm">
|
||||
Aucune colonne visible
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<DroppableColumn
|
||||
status="todo"
|
||||
tasks={todoTasks}
|
||||
title="À faire"
|
||||
color="bg-[var(--primary)]"
|
||||
icon="📋"
|
||||
onEditTask={onEditTask}
|
||||
compactView={compactView}
|
||||
/>
|
||||
|
||||
<DroppableColumn
|
||||
status="in_progress"
|
||||
tasks={inProgressTasks}
|
||||
title="En cours"
|
||||
color="bg-yellow-400"
|
||||
icon="🔄"
|
||||
onEditTask={onEditTask}
|
||||
compactView={compactView}
|
||||
/>
|
||||
|
||||
<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}
|
||||
<>
|
||||
{/* Headers des colonnes */}
|
||||
<div
|
||||
className={`grid gap-4 px-6 py-4`}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${columnsWithTasks.length}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{columnsWithTasks.map(({ config }) => {
|
||||
const style = getTechStyle(config.color);
|
||||
const tasksInStatus = tasksByStatus[config.key] || [];
|
||||
return (
|
||||
<div key={config.key} className="text-center">
|
||||
<ColumnHeader
|
||||
title={config.label}
|
||||
icon={config.icon}
|
||||
count={tasksInStatus.length}
|
||||
color={style.accent.replace('text-', '')}
|
||||
accentColor={style.accent}
|
||||
className="justify-center gap-4"
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!isMounted) {
|
||||
|
||||
@@ -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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user