Files
towercontrol/components/kanban/KanbanFilters.tsx
Julien Froidefond 5e09759c2b feat: enhance Kanban filtering and integrate filters in BoardContainer
- Marked multiple tasks as completed in TODO.md related to Kanban filtering features.
- Added `KanbanFilters` component to `BoardContainer` for improved task filtering.
- Updated `TasksContext` to manage Kanban filters and provide filtered tasks to the board.
- Implemented real-time filtering logic based on search, tags, and priorities.
2025-09-14 16:48:41 +02:00

199 lines
7.3 KiB
TypeScript

'use client';
import { useState } from 'react';
import { TaskPriority } from '@/lib/types';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { useTasksContext } from '@/contexts/TasksContext';
export interface KanbanFilters {
search?: string;
tags?: string[];
priorities?: TaskPriority[];
showCompleted?: boolean;
}
interface KanbanFiltersProps {
filters: KanbanFilters;
onFiltersChange: (filters: KanbanFilters) => void;
}
export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps) {
const { tags: availableTags } = useTasksContext();
const [isExpanded, setIsExpanded] = useState(false);
const handleSearchChange = (search: string) => {
onFiltersChange({ ...filters, search: search || undefined });
};
const handleTagToggle = (tagName: string) => {
const currentTags = filters.tags || [];
const newTags = currentTags.includes(tagName)
? currentTags.filter(t => t !== tagName)
: [...currentTags, tagName];
onFiltersChange({
...filters,
tags: newTags.length > 0 ? newTags : undefined
});
};
const handlePriorityToggle = (priority: TaskPriority) => {
const currentPriorities = filters.priorities || [];
const newPriorities = currentPriorities.includes(priority)
? currentPriorities.filter(p => p !== priority)
: [...currentPriorities, priority];
onFiltersChange({
...filters,
priorities: newPriorities.length > 0 ? newPriorities : undefined
});
};
const handleClearFilters = () => {
onFiltersChange({});
};
const hasActiveFilters = filters.search || filters.tags?.length || filters.priorities?.length;
const priorityOptions: { value: TaskPriority; label: string; color: string }[] = [
{ value: 'urgent', label: 'Urgent', color: 'bg-red-500' },
{ value: 'high', label: 'Haute', color: 'bg-orange-500' },
{ value: 'medium', label: 'Moyenne', color: 'bg-yellow-500' },
{ value: 'low', label: 'Basse', color: 'bg-blue-500' }
];
return (
<div className="bg-slate-900/50 border-b border-slate-700/50 backdrop-blur-sm">
<div className="container mx-auto px-6 py-4">
{/* Header avec recherche et bouton expand */}
<div className="flex items-center gap-4">
<div className="flex-1 max-w-md">
<Input
type="text"
value={filters.search || ''}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Rechercher des tâches..."
className="bg-slate-800/50 border-slate-600"
/>
</div>
<Button
variant="ghost"
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-2"
>
<svg
className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
Filtres
{hasActiveFilters && (
<span className="bg-cyan-500 text-slate-900 text-xs px-2 py-0.5 rounded-full font-medium">
{(filters.tags?.length || 0) + (filters.priorities?.length || 0) + (filters.search ? 1 : 0)}
</span>
)}
</Button>
{hasActiveFilters && (
<Button
variant="ghost"
onClick={handleClearFilters}
className="text-slate-400 hover:text-red-400"
>
Effacer
</Button>
)}
</div>
{/* Filtres étendus */}
{isExpanded && (
<div className="mt-4 space-y-4 border-t border-slate-700/50 pt-4">
{/* Filtres par priorité */}
<div className="space-y-2">
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
Priorités
</label>
<div className="flex flex-wrap gap-2">
{priorityOptions.map((priority) => (
<button
key={priority.value}
onClick={() => handlePriorityToggle(priority.value)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border transition-all text-sm font-medium ${
filters.priorities?.includes(priority.value)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-slate-600 bg-slate-800/50 text-slate-400 hover:border-slate-500'
}`}
>
<div className={`w-2 h-2 rounded-full ${priority.color}`} />
{priority.label}
</button>
))}
</div>
</div>
{/* Filtres par tags */}
{availableTags.length > 0 && (
<div className="space-y-2">
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
Tags
</label>
<div className="flex flex-wrap gap-2">
{availableTags.map((tag) => (
<button
key={tag.id}
onClick={() => handleTagToggle(tag.name)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border transition-all text-sm font-medium ${
filters.tags?.includes(tag.name)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-slate-600 bg-slate-800/50 text-slate-400 hover:border-slate-500'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: tag.color }}
/>
{tag.name}
</button>
))}
</div>
</div>
)}
{/* Résumé des filtres actifs */}
{hasActiveFilters && (
<div className="bg-slate-800/30 rounded-lg p-3 border border-slate-700/50">
<div className="text-xs text-slate-400 font-mono uppercase tracking-wider mb-2">
Filtres actifs
</div>
<div className="space-y-1 text-sm">
{filters.search && (
<div className="text-slate-300">
Recherche: <span className="text-cyan-400">"{filters.search}"</span>
</div>
)}
{filters.priorities?.length && (
<div className="text-slate-300">
Priorités: <span className="text-cyan-400">{filters.priorities.join(', ')}</span>
</div>
)}
{filters.tags?.length && (
<div className="text-slate-300">
Tags: <span className="text-cyan-400">{filters.tags.join(', ')}</span>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}