- 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.
312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
'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>
|
||
);
|
||
}
|