Files
towercontrol/components/jira/FilterBar.tsx
Julien Froidefond 3dd6e0fd1c feat: enhance Jira dashboard with advanced filtering and sprint details
- Updated `TODO.md` to mark several tasks as complete, including anomaly detection and sprint detail integration.
- Enhanced `VelocityChart` to support click events for sprint details, improving user interaction.
- Added `FilterBar` and `AnomalyDetectionPanel` components to `JiraDashboardPageClient` for advanced filtering capabilities.
- Implemented state management for selected sprints and modal display for detailed sprint information.
- Introduced new types for advanced filtering in `types.ts`, expanding the filtering options available in the analytics.
2025-09-19 10:13:48 +02:00

312 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } from 'react';
import { JiraAnalyticsFilters, AvailableFilters } from '@/lib/types';
import { JiraAdvancedFiltersService } from '@/services/jira-advanced-filters';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Modal } from '@/components/ui/Modal';
interface FilterBarProps {
availableFilters: AvailableFilters;
activeFilters: Partial<JiraAnalyticsFilters>;
onFiltersChange: (filters: Partial<JiraAnalyticsFilters>) => void;
className?: string;
}
export default function FilterBar({
availableFilters,
activeFilters,
onFiltersChange,
className = ''
}: FilterBarProps) {
const [showModal, setShowModal] = useState(false);
const hasActiveFilters = JiraAdvancedFiltersService.hasActiveFilters(activeFilters);
const activeFiltersCount = JiraAdvancedFiltersService.countActiveFilters(activeFilters);
const clearAllFilters = () => {
const emptyFilters = JiraAdvancedFiltersService.createEmptyFilters();
onFiltersChange(emptyFilters);
};
const removeFilter = (filterType: keyof JiraAnalyticsFilters, value: string) => {
const currentValues = activeFilters[filterType];
if (!currentValues || !Array.isArray(currentValues)) return;
const newValues = currentValues.filter((v: string) => v !== value);
onFiltersChange({
...activeFilters,
[filterType]: newValues
});
};
return (
<div className={`bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 ${className}`}>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-[var(--foreground)]">🔍 Filtres</span>
{hasActiveFilters && (
<Badge className="bg-blue-100 text-blue-800 text-xs">
{activeFiltersCount}
</Badge>
)}
</div>
{/* Filtres actifs */}
{hasActiveFilters && (
<div className="flex flex-wrap gap-1 max-w-2xl overflow-hidden">
{activeFilters.components?.slice(0, 3).map(comp => (
<Badge
key={comp}
className="bg-purple-100 text-purple-800 text-xs cursor-pointer hover:bg-purple-200 transition-colors"
onClick={() => removeFilter('components', comp)}
>
📦 {comp} ×
</Badge>
))}
{activeFilters.fixVersions?.slice(0, 2).map(version => (
<Badge
key={version}
className="bg-green-100 text-green-800 text-xs cursor-pointer hover:bg-green-200 transition-colors"
onClick={() => removeFilter('fixVersions', version)}
>
🏷 {version} ×
</Badge>
))}
{activeFilters.issueTypes?.slice(0, 3).map(type => (
<Badge
key={type}
className="bg-orange-100 text-orange-800 text-xs cursor-pointer hover:bg-orange-200 transition-colors"
onClick={() => removeFilter('issueTypes', type)}
>
📋 {type} ×
</Badge>
))}
{activeFilters.statuses?.slice(0, 2).map(status => (
<Badge
key={status}
className="bg-blue-100 text-blue-800 text-xs cursor-pointer hover:bg-blue-200 transition-colors"
onClick={() => removeFilter('statuses', status)}
>
🔄 {status} ×
</Badge>
))}
{activeFilters.assignees?.slice(0, 2).map(assignee => (
<Badge
key={assignee}
className="bg-yellow-100 text-yellow-800 text-xs cursor-pointer hover:bg-yellow-200 transition-colors"
onClick={() => removeFilter('assignees', assignee)}
>
👤 {assignee} ×
</Badge>
))}
{/* Indicateur si plus de filtres */}
{(() => {
const totalVisible =
(activeFilters.components?.slice(0, 3).length || 0) +
(activeFilters.fixVersions?.slice(0, 2).length || 0) +
(activeFilters.issueTypes?.slice(0, 3).length || 0) +
(activeFilters.statuses?.slice(0, 2).length || 0) +
(activeFilters.assignees?.slice(0, 2).length || 0);
const totalActive = activeFiltersCount;
if (totalActive > totalVisible) {
return (
<Badge className="bg-gray-100 text-gray-800 text-xs">
+{totalActive - totalVisible} autres
</Badge>
);
}
return null;
})()}
</div>
)}
{!hasActiveFilters && (
<span className="text-sm text-[var(--muted-foreground)]">
Aucun filtre actif
</span>
)}
</div>
<div className="flex items-center gap-2">
{hasActiveFilters && (
<Button
onClick={clearAllFilters}
variant="secondary"
size="sm"
className="text-xs"
>
Effacer
</Button>
)}
<Button
onClick={() => setShowModal(true)}
variant="primary"
size="sm"
className="text-xs"
>
Configurer
</Button>
</div>
</div>
{/* Modal de configuration - réutilise la logique du composant existant */}
{showModal && (
<Modal
isOpen={showModal}
onClose={() => setShowModal(false)}
title="Configuration des filtres"
size="lg"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-h-96 overflow-y-auto">
{/* Types de tickets */}
<div>
<h4 className="font-medium text-sm mb-3">📋 Types de tickets</h4>
<div className="space-y-1 max-h-32 overflow-y-auto">
{availableFilters.issueTypes.map(option => (
<label
key={option.value}
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-[var(--muted)] px-2 py-1 rounded"
>
<input
type="checkbox"
checked={activeFilters.issueTypes?.includes(option.value) || false}
onChange={(e) => {
const current = activeFilters.issueTypes || [];
const newValues = e.target.checked
? [...current, option.value]
: current.filter(v => v !== option.value);
onFiltersChange({
...activeFilters,
issueTypes: newValues
});
}}
className="rounded"
/>
<span className="flex-1 truncate">{option.label}</span>
<span className="text-xs text-[var(--muted-foreground)]">({option.count})</span>
</label>
))}
</div>
</div>
{/* Statuts */}
<div>
<h4 className="font-medium text-sm mb-3">🔄 Statuts</h4>
<div className="space-y-1 max-h-32 overflow-y-auto">
{availableFilters.statuses.map(option => (
<label
key={option.value}
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-[var(--muted)] px-2 py-1 rounded"
>
<input
type="checkbox"
checked={activeFilters.statuses?.includes(option.value) || false}
onChange={(e) => {
const current = activeFilters.statuses || [];
const newValues = e.target.checked
? [...current, option.value]
: current.filter(v => v !== option.value);
onFiltersChange({
...activeFilters,
statuses: newValues
});
}}
className="rounded"
/>
<span className="flex-1 truncate">{option.label}</span>
<span className="text-xs text-[var(--muted-foreground)]">({option.count})</span>
</label>
))}
</div>
</div>
{/* Assignés */}
<div>
<h4 className="font-medium text-sm mb-3">👤 Assignés</h4>
<div className="space-y-1 max-h-32 overflow-y-auto">
{availableFilters.assignees.map(option => (
<label
key={option.value}
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-[var(--muted)] px-2 py-1 rounded"
>
<input
type="checkbox"
checked={activeFilters.assignees?.includes(option.value) || false}
onChange={(e) => {
const current = activeFilters.assignees || [];
const newValues = e.target.checked
? [...current, option.value]
: current.filter(v => v !== option.value);
onFiltersChange({
...activeFilters,
assignees: newValues
});
}}
className="rounded"
/>
<span className="flex-1 truncate">{option.label}</span>
<span className="text-xs text-[var(--muted-foreground)]">({option.count})</span>
</label>
))}
</div>
</div>
{/* Composants */}
<div>
<h4 className="font-medium text-sm mb-3">📦 Composants</h4>
<div className="space-y-1 max-h-32 overflow-y-auto">
{availableFilters.components.map(option => (
<label
key={option.value}
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-[var(--muted)] px-2 py-1 rounded"
>
<input
type="checkbox"
checked={activeFilters.components?.includes(option.value) || false}
onChange={(e) => {
const current = activeFilters.components || [];
const newValues = e.target.checked
? [...current, option.value]
: current.filter(v => v !== option.value);
onFiltersChange({
...activeFilters,
components: newValues
});
}}
className="rounded"
/>
<span className="flex-1 truncate">{option.label}</span>
<span className="text-xs text-[var(--muted-foreground)]">({option.count})</span>
</label>
))}
</div>
</div>
</div>
<div className="flex gap-2 pt-6 border-t">
<Button
onClick={() => setShowModal(false)}
className="flex-1"
>
Fermer
</Button>
<Button
onClick={clearAllFilters}
variant="secondary"
className="flex-1"
>
🗑 Effacer tout
</Button>
</div>
</Modal>
)}
</div>
);
}