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-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 {
|
||||||
|
|||||||
@@ -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 => {
|
||||||
// Exclure les tâches avec des tags marqués comme "objectif principal" (isPinned = true)
|
return ['done', 'cancelled', 'archived'].includes(status);
|
||||||
if (task.tagDetails && task.tagDetails.some((tag) => tag.isPinned)) {
|
};
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrer selon la recherche
|
// Filtrer et trier les tâches selon la recherche et exclure les tâches avec des tags "objectif principal"
|
||||||
return (
|
const filteredTasks = allTasks
|
||||||
task.title.toLowerCase().includes(taskSearch.toLowerCase()) ||
|
.filter((task) => {
|
||||||
(task.description &&
|
// Exclure les tâches avec des tags marqués comme "objectif principal" (isPinned = true)
|
||||||
task.description.toLowerCase().includes(taskSearch.toLowerCase()))
|
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) => {
|
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,45 +288,78 @@ 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>
|
||||||
<button
|
{filteredTasks.slice(0, 8).map((task, index) => {
|
||||||
key={task.id}
|
const isCompleted = isCompletedStatus(task.status);
|
||||||
type="button"
|
return (
|
||||||
onClick={() => handleTaskSelect(task)}
|
<div key={task.id}>
|
||||||
className="w-full text-left p-3 hover:bg-[var(--muted)]/50 transition-colors border-b border-[var(--border)]/30 last:border-b-0"
|
{index > 0 && (
|
||||||
disabled={saving}
|
<div className="h-px bg-[color-mix(in_srgb,var(--border)_10%,transparent)]" />
|
||||||
>
|
)}
|
||||||
<div className="font-medium text-sm truncate">
|
<button
|
||||||
{task.title}
|
type="button"
|
||||||
</div>
|
onClick={() => handleTaskSelect(task)}
|
||||||
{task.description && (
|
className={`
|
||||||
<div className="text-xs text-[var(--muted-foreground)] truncate mt-1 max-w-full overflow-hidden">
|
w-full text-left p-3 transition-all duration-200
|
||||||
{task.description}
|
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
|
||||||
|
${isCompleted ? 'text-[var(--muted-foreground)]' : 'text-[var(--foreground)]'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{task.title}
|
||||||
|
</div>
|
||||||
|
{task.description && (
|
||||||
|
<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-2 flex-wrap">
|
||||||
|
<StatusBadge
|
||||||
|
status={task.status}
|
||||||
|
className={isCompleted ? 'opacity-70' : ''}
|
||||||
|
/>
|
||||||
|
{task.tags && task.tags.length > 0 && (
|
||||||
|
<TagDisplay
|
||||||
|
tags={task.tags}
|
||||||
|
size="sm"
|
||||||
|
availableTags={task.tagDetails}
|
||||||
|
maxTags={3}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
<div className="flex items-center gap-2 mt-1">
|
})}
|
||||||
<StatusBadge status={task.status} />
|
</div>
|
||||||
{task.tags && task.tags.length > 0 && (
|
|
||||||
<TagDisplay
|
|
||||||
tags={task.tags}
|
|
||||||
size="sm"
|
|
||||||
availableTags={task.tagDetails}
|
|
||||||
maxTags={3}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export function KanbanBoardContainer({
|
|||||||
onEditTask={handleEditTask}
|
onEditTask={handleEditTask}
|
||||||
onUpdateStatus={handleUpdateStatus}
|
onUpdateStatus={handleUpdateStatus}
|
||||||
pinnedTagName={pinnedTagName}
|
pinnedTagName={pinnedTagName}
|
||||||
|
visibleStatuses={visibleStatuses}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BoardRouter
|
<BoardRouter
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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">
|
<SortableContext
|
||||||
<div className={`w-2 h-2 rounded-full ${color}`}></div>
|
items={tasks.map((t) => t.id)}
|
||||||
<h3
|
strategy={verticalListSortingStrategy}
|
||||||
className={`text-sm font-mono font-medium uppercase tracking-wider ${color.replace('bg-', 'text-').replace('400', '300')}`}
|
>
|
||||||
>
|
<div className="space-y-3">
|
||||||
{title}
|
{tasks.map((task) => (
|
||||||
</h3>
|
<TaskCard
|
||||||
<div className="flex-1"></div>
|
key={task.id}
|
||||||
<span className="text-xs text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
task={task}
|
||||||
{tasks.length}
|
onEdit={onEditTask}
|
||||||
</span>
|
compactView={compactView}
|
||||||
</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>
|
</div>
|
||||||
) : (
|
</SortableContext>
|
||||||
<SortableContext
|
</DropZone>
|
||||||
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
|
|
||||||
task={task}
|
|
||||||
onEdit={onEditTask}
|
|
||||||
compactView={compactView}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</SortableContext>
|
|
||||||
)}
|
|
||||||
</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,121 +122,142 @@ 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 px-6">
|
||||||
<CardHeader className="pb-4">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<button
|
||||||
<button
|
onClick={handleToggleCollapse}
|
||||||
onClick={toggleObjectivesCollapse}
|
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>
|
<h2 className="text-lg font-mono font-bold text-[var(--accent)] uppercase tracking-wider">
|
||||||
<h2 className="text-lg font-mono font-bold text-[var(--accent)] uppercase tracking-wider">
|
<Emoji emoji="🎯" /> Objectifs Principaux
|
||||||
<Emoji emoji="🎯" /> Objectifs Principaux
|
</h2>
|
||||||
</h2>
|
{pinnedTagName && (
|
||||||
{pinnedTagName && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="border-[var(--accent)]/50 text-[var(--accent)]"
|
|
||||||
>
|
|
||||||
{pinnedTagName}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge
|
<Badge
|
||||||
variant="primary"
|
variant="outline"
|
||||||
size="sm"
|
className="border-[var(--accent)]/50 text-[var(--accent)]"
|
||||||
className="bg-[var(--accent)]"
|
|
||||||
>
|
>
|
||||||
{String(tasks.length).padStart(2, '0')}
|
{pinnedTagName}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Bouton collapse séparé pour mobile */}
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<Badge variant="primary" size="sm" className="bg-[var(--accent)]">
|
||||||
onClick={toggleObjectivesCollapse}
|
{String(tasks.length).padStart(2, '0')}
|
||||||
className="lg:hidden p-1 hover:bg-[var(--accent)]/20 rounded transition-colors"
|
</Badge>
|
||||||
aria-label={isCollapsed ? 'Développer' : 'Réduire'}
|
|
||||||
>
|
{/* Bouton collapse séparé pour mobile */}
|
||||||
<ChevronDown
|
<button
|
||||||
className={`w-4 h-4 text-[var(--accent)] transition-transform duration-200 ${
|
onClick={handleToggleCollapse}
|
||||||
isCollapsed ? 'rotate-180' : ''
|
className="lg:hidden p-1 hover:bg-[var(--accent)]/20 rounded transition-colors"
|
||||||
}`}
|
aria-label={isCollapsed ? 'Développer' : 'Réduire'}
|
||||||
/>
|
>
|
||||||
</button>
|
<ChevronDown
|
||||||
</div>
|
className={`w-4 h-4 text-[var(--accent)] transition-transform duration-200 ${
|
||||||
|
isCollapsed ? 'rotate-180' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</div>
|
||||||
|
</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'
|
|
||||||
);
|
|
||||||
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'
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// 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[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm">
|
||||||
<DroppableColumn
|
Aucune colonne visible
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
}
|
||||||
</CardContent>
|
|
||||||
)}
|
return (
|
||||||
</Card>
|
<>
|
||||||
</div>
|
{/* 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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user