- Updated `CreateTaskForm` and `EditTaskForm` to load priority options dynamically using `getAllPriorities`, improving maintainability. - Refactored `KanbanFilters` to utilize dynamic priority options, enhancing filter functionality. - Modified `QuickAddTask` and `TaskCard` to display priorities using centralized configuration, ensuring consistency across the application. - Introduced new utility functions in `status-config.ts` for managing priority configurations, streamlining the task management process.
262 lines
9.5 KiB
TypeScript
262 lines
9.5 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 { useTasksContext } from '@/contexts/TasksContext';
|
|
import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
|
|
|
|
export interface KanbanFilters {
|
|
search?: string;
|
|
tags?: string[];
|
|
priorities?: TaskPriority[];
|
|
showCompleted?: boolean;
|
|
compactView?: boolean;
|
|
swimlanesByTags?: boolean;
|
|
pinnedTag?: string; // Tag pour les objectifs principaux
|
|
}
|
|
|
|
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 handleCompactViewToggle = () => {
|
|
onFiltersChange({
|
|
...filters,
|
|
compactView: !filters.compactView
|
|
});
|
|
};
|
|
|
|
const handleSwimlanesToggle = () => {
|
|
onFiltersChange({
|
|
...filters,
|
|
swimlanesByTags: !filters.swimlanesByTags
|
|
});
|
|
};
|
|
|
|
const handleClearFilters = () => {
|
|
onFiltersChange({});
|
|
};
|
|
|
|
const activeFiltersCount = (filters.tags?.length || 0) + (filters.priorities?.length || 0) + (filters.search ? 1 : 0);
|
|
|
|
const priorityOptions = getAllPriorities().map(priorityConfig => ({
|
|
value: priorityConfig.key,
|
|
label: priorityConfig.label,
|
|
color: priorityConfig.color
|
|
}));
|
|
|
|
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>
|
|
|
|
{/* Bouton swimlanes par tags */}
|
|
<Button
|
|
variant={filters.swimlanesByTags ? "primary" : "ghost"}
|
|
onClick={handleSwimlanesToggle}
|
|
className="flex items-center gap-2"
|
|
title={filters.swimlanesByTags ? "Vue normale" : "Lignes par tags"}
|
|
>
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
{filters.swimlanesByTags ? (
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
|
) : (
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
)}
|
|
</svg>
|
|
{filters.swimlanesByTags ? 'Normal' : 'Swimlanes'}
|
|
</Button>
|
|
|
|
{/* Bouton vue compacte */}
|
|
<Button
|
|
variant={filters.compactView ? "primary" : "ghost"}
|
|
onClick={handleCompactViewToggle}
|
|
className="flex items-center gap-2"
|
|
title={filters.compactView ? "Vue détaillée" : "Vue compacte"}
|
|
>
|
|
<svg
|
|
className="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
{filters.compactView ? (
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
|
) : (
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
)}
|
|
</svg>
|
|
{filters.compactView ? 'Détaillée' : 'Compacte'}
|
|
</Button>
|
|
|
|
<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
|
|
{activeFiltersCount > 0 && (
|
|
<span className="bg-cyan-500 text-slate-900 text-xs px-2 py-0.5 rounded-full font-medium">
|
|
{activeFiltersCount}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
|
|
{activeFiltersCount > 0 && (
|
|
<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"
|
|
style={{ backgroundColor: getPriorityColorHex(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 */}
|
|
{activeFiltersCount > 0 && (
|
|
<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>
|
|
);
|
|
}
|