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.
This commit is contained in:
Julien Froidefond
2025-09-19 10:13:48 +02:00
parent b7707d7651
commit 3dd6e0fd1c
17 changed files with 2879 additions and 68 deletions

View File

@@ -0,0 +1,311 @@
'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>
);
}