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:
311
components/jira/FilterBar.tsx
Normal file
311
components/jira/FilterBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user