'use client'; import { useState, useEffect, useRef, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { TaskPriority, TaskStatus } 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'; import { SORT_OPTIONS } from '@/lib/sort-config'; import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { ColumnVisibilityToggle } from './ColumnVisibilityToggle'; export interface KanbanFilters { search?: string; tags?: string[]; priorities?: TaskPriority[]; showCompleted?: boolean; compactView?: boolean; swimlanesByTags?: boolean; swimlanesMode?: 'tags' | 'priority'; // Mode des swimlanes pinnedTag?: string; // Tag pour les objectifs principaux sortBy?: string; // Clé de l'option de tri sélectionnée // Filtres spécifiques Jira showJiraOnly?: boolean; // Afficher seulement les tâches Jira hideJiraTasks?: boolean; // Masquer toutes les tâches Jira jiraProjects?: string[]; // Filtrer par projet Jira jiraTypes?: string[]; // Filtrer par type Jira (Story, Task, Bug, etc.) } interface KanbanFiltersProps { filters: KanbanFilters; onFiltersChange: (filters: KanbanFilters) => void; hiddenStatuses?: Set; onToggleStatusVisibility?: (status: TaskStatus) => void; } export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsHiddenStatuses, onToggleStatusVisibility }: KanbanFiltersProps) { const { tags: availableTags, regularTasks } = useTasksContext(); const { preferences, toggleColumnVisibility } = useUserPreferences(); // Utiliser les props si disponibles, sinon utiliser le context const hiddenStatuses = propsHiddenStatuses || new Set(preferences.columnVisibility.hiddenStatuses); const toggleStatusVisibility = onToggleStatusVisibility || toggleColumnVisibility; const [isExpanded, setIsExpanded] = useState(false); const [isSortExpanded, setIsSortExpanded] = useState(false); const [isSwimlaneModeExpanded, setIsSwimlaneModeExpanded] = useState(false); const sortDropdownRef = useRef(null); const swimlaneModeDropdownRef = useRef(null); const sortButtonRef = useRef(null); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); // Fermer les dropdowns en cliquant à l'extérieur useEffect(() => { function handleClickOutside(event: MouseEvent) { if (sortDropdownRef.current && !sortDropdownRef.current.contains(event.target as Node)) { setIsSortExpanded(false); } if (swimlaneModeDropdownRef.current && !swimlaneModeDropdownRef.current.contains(event.target as Node)) { setIsSwimlaneModeExpanded(false); } } if (isSortExpanded || isSwimlaneModeExpanded) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isSortExpanded, isSwimlaneModeExpanded]); 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 handleSwimlanesToggle = () => { onFiltersChange({ ...filters, swimlanesByTags: !filters.swimlanesByTags }); }; const handleSwimlaneModeChange = (mode: 'tags' | 'priority') => { onFiltersChange({ ...filters, swimlanesByTags: true, swimlanesMode: mode }); setIsSwimlaneModeExpanded(false); }; const handleSwimlaneModeToggle = (event: React.MouseEvent) => { const button = event.currentTarget; const rect = button.getBoundingClientRect(); setDropdownPosition({ top: rect.bottom + window.scrollY + 4, left: rect.left + window.scrollX }); setIsSwimlaneModeExpanded(!isSwimlaneModeExpanded); }; const handleSortChange = (sortKey: string) => { onFiltersChange({ ...filters, sortBy: sortKey }); }; const handleSortToggle = () => { if (!isSortExpanded && sortButtonRef.current) { const rect = sortButtonRef.current.getBoundingClientRect(); setDropdownPosition({ top: rect.bottom + window.scrollY + 4, left: rect.left + window.scrollX }); } setIsSortExpanded(!isSortExpanded); }; const handleJiraToggle = (mode: 'show' | 'hide' | 'all') => { const updates: Partial = {}; switch (mode) { case 'show': updates.showJiraOnly = true; updates.hideJiraTasks = false; break; case 'hide': updates.showJiraOnly = false; updates.hideJiraTasks = true; break; case 'all': updates.showJiraOnly = false; updates.hideJiraTasks = false; break; } onFiltersChange({ ...filters, ...updates }); }; const handleJiraProjectToggle = (project: string) => { const currentProjects = filters.jiraProjects || []; const newProjects = currentProjects.includes(project) ? currentProjects.filter(p => p !== project) : [...currentProjects, project]; onFiltersChange({ ...filters, jiraProjects: newProjects.length > 0 ? newProjects : undefined }); }; const handleJiraTypeToggle = (type: string) => { const currentTypes = filters.jiraTypes || []; const newTypes = currentTypes.includes(type) ? currentTypes.filter(t => t !== type) : [...currentTypes, type]; onFiltersChange({ ...filters, jiraTypes: newTypes.length > 0 ? newTypes : undefined }); }; const handleClearFilters = () => { onFiltersChange({}); }; // Récupérer les projets et types Jira disponibles dans TOUTES les tâches (pas seulement les filtrées) // regularTasks est déjà disponible depuis la ligne 39 const availableJiraProjects = useMemo(() => { const projects = new Set(); regularTasks.forEach(task => { if (task.source === 'jira' && task.jiraProject) { projects.add(task.jiraProject); } }); return Array.from(projects).sort(); }, [regularTasks]); const availableJiraTypes = useMemo(() => { const types = new Set(); regularTasks.forEach(task => { if (task.source === 'jira' && task.jiraType) { types.add(task.jiraType); } }); return Array.from(types).sort(); }, [regularTasks]); // Vérifier s'il y a des tâches Jira dans le système (même masquées) const hasJiraTasks = regularTasks.some(task => task.source === 'jira'); const activeFiltersCount = (filters.tags?.filter(Boolean).length || 0) + (filters.priorities?.filter(Boolean).length || 0) + (filters.search ? 1 : 0) + (filters.jiraProjects?.filter(Boolean).length || 0) + (filters.jiraTypes?.filter(Boolean).length || 0) + (filters.showJiraOnly ? 1 : 0) + (filters.hideJiraTasks ? 1 : 0); // Calculer les compteurs pour les priorités const priorityCounts = useMemo(() => { const counts: Record = {}; getAllPriorities().forEach(priority => { counts[priority.key] = regularTasks.filter(task => task.priority === priority.key).length; }); return counts; }, [regularTasks]); // Calculer les compteurs pour les tags const tagCounts = useMemo(() => { const counts: Record = {}; availableTags.forEach(tag => { counts[tag.name] = regularTasks.filter(task => task.tags?.includes(tag.name)).length; }); return counts; }, [regularTasks, availableTags]); const priorityOptions = getAllPriorities().map(priorityConfig => ({ value: priorityConfig.key, label: priorityConfig.label, color: priorityConfig.color, count: priorityCounts[priorityConfig.key] || 0 })); // Trier les tags par nombre d'utilisation (décroissant) const sortedTags = useMemo(() => { return [...availableTags].sort((a, b) => { const countA = tagCounts[a.name] || 0; const countB = tagCounts[b.name] || 0; return countB - countA; // Décroissant }); }, [availableTags, tagCounts]); return (
{/* Header avec recherche et bouton expand */}
handleSearchChange(e.target.value)} placeholder="Rechercher des tâches..." className="bg-[var(--card)] border-[var(--border)]" />
{/* Menu swimlanes */}
{/* Bouton pour changer le mode des swimlanes */} {filters.swimlanesByTags && ( )}
{/* Bouton de tri */}
{activeFiltersCount > 0 && ( )}
{/* Filtres étendus */} {isExpanded && (
{/* Grille responsive pour les filtres principaux */}
{/* Filtres par priorité */}
{priorityOptions.filter(priority => priority.count > 0).map((priority) => ( ))}
{/* Filtres par tags */} {availableTags.length > 0 && (
{sortedTags.filter(tag => (tagCounts[tag.name] || 0) > 0).map((tag) => ( ))}
)}
{/* Filtres Jira - Ligne séparée mais intégrée */} {hasJiraTasks && (
{/* Toggle Jira Show/Hide - inline avec le titre */}
{/* Projets et Types en 2 colonnes */}
{/* Projets Jira */} {availableJiraProjects.length > 0 && (
{availableJiraProjects.map((project) => ( ))}
)} {/* Types Jira */} {availableJiraTypes.length > 0 && (
{availableJiraTypes.map((type) => ( ))}
)}
)} {/* Visibilité des colonnes */}
{/* Résumé des filtres actifs */} {activeFiltersCount > 0 && (
Filtres actifs
{filters.search && (
Recherche: “{filters.search}”
)} {(filters.priorities?.filter(Boolean).length || 0) > 0 && (
Priorités: {filters.priorities?.filter(Boolean).join(', ')}
)} {(filters.tags?.filter(Boolean).length || 0) > 0 && (
Tags: {filters.tags?.filter(Boolean).join(', ')}
)} {filters.showJiraOnly && (
Affichage: Jira seulement
)} {filters.hideJiraTasks && (
Affichage: Masquer Jira
)} {(filters.jiraProjects?.filter(Boolean).length || 0) > 0 && (
Projets Jira: {filters.jiraProjects?.filter(Boolean).join(', ')}
)} {(filters.jiraTypes?.filter(Boolean).length || 0) > 0 && (
Types Jira: {filters.jiraTypes?.filter(Boolean).join(', ')}
)}
)}
)}
{/* Dropdown de tri rendu via portail pour éviter les problèmes de z-index */} {isSortExpanded && typeof window !== 'undefined' && createPortal(
{SORT_OPTIONS.map((option) => ( ))}
, document.body )} {/* Dropdown des modes swimlanes rendu via portail pour éviter les problèmes de z-index */} {isSwimlaneModeExpanded && typeof window !== 'undefined' && createPortal(
, document.body )}
); }