feat: enhance JiraDashboardPage with new components and improved UI

- Integrated `PeriodSelector`, `SkeletonGrid`, and `MetricsGrid` for better data visualization and user interaction.
- Replaced legacy period selection and error display with new components for a cleaner UI.
- Updated `UIShowcaseClient` to demonstrate new Jira dashboard components, enhancing showcase functionality.
This commit is contained in:
Julien Froidefond
2025-09-29 16:47:35 +02:00
parent 6c0c353a4e
commit c1a14f9196
6 changed files with 304 additions and 101 deletions

View File

@@ -9,6 +9,9 @@ import { Header } from '@/components/ui/Header';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { PeriodSelector, SkeletonGrid, MetricsGrid } from '@/components/ui';
import { AlertBanner } from '@/components/ui/AlertBanner';
import { Tabs } from '@/components/ui/Tabs';
import { VelocityChart } from '@/components/jira/VelocityChart'; import { VelocityChart } from '@/components/jira/VelocityChart';
import { TeamDistributionChart } from '@/components/jira/TeamDistributionChart'; import { TeamDistributionChart } from '@/components/jira/TeamDistributionChart';
import { CycleTimeChart } from '@/components/jira/CycleTimeChart'; import { CycleTimeChart } from '@/components/jira/CycleTimeChart';
@@ -197,26 +200,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Sélecteur de période */} {/* Sélecteur de période */}
<div className="flex bg-[var(--card)] border border-[var(--border)] rounded-lg p-1"> <PeriodSelector
{[ options={[
{ value: '7d', label: '7j' }, { value: '7d', label: '7j' },
{ value: '30d', label: '30j' }, { value: '30d', label: '30j' },
{ value: '3m', label: '3m' }, { value: '3m', label: '3m' },
{ value: 'current', label: 'Sprint' } { value: 'current', label: 'Sprint' }
].map((period: { value: string; label: string }) => ( ]}
<button selectedValue={selectedPeriod}
key={period.value} onValueChange={(value) => setSelectedPeriod(value as PeriodFilter)}
onClick={() => setSelectedPeriod(period.value as PeriodFilter)} />
className={`px-3 py-1 text-sm rounded transition-all ${
selectedPeriod === period.value
? 'bg-[var(--primary)] text-[var(--primary-foreground)]'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
{period.label}
</button>
))}
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{analytics && ( {analytics && (
@@ -260,40 +253,27 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
{/* Contenu principal */} {/* Contenu principal */}
{error && ( {error && (
<Card className="mb-6 border-red-500/20 bg-red-500/10"> <AlertBanner
<CardContent className="p-4"> title="Erreur"
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}> items={[{ id: 'error', title: error }]}
<span></span> icon="❌"
<span>{error}</span> variant="error"
</div> className="mb-6"
</CardContent> />
</Card>
)} )}
{exportError && ( {exportError && (
<Card className="mb-6 border-orange-500/20 bg-orange-500/10"> <AlertBanner
<CardContent className="p-4"> title="Erreur d'export"
<div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}> items={[{ id: 'export-error', title: exportError }]}
<span></span> icon="⚠️"
<span>Erreur d&apos;export: {exportError}</span> variant="warning"
</div> className="mb-6"
</CardContent> />
</Card>
)} )}
{isLoading && !analytics && ( {isLoading && !analytics && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <SkeletonGrid count={6} />
{/* Skeleton loading */}
{[1, 2, 3, 4, 5, 6].map(i => (
<Card key={i} className="animate-pulse">
<CardContent className="p-6">
<div className="h-4 bg-[var(--muted)] rounded mb-4"></div>
<div className="h-8 bg-[var(--muted)] rounded mb-2"></div>
<div className="h-4 bg-[var(--muted)] rounded w-2/3"></div>
</CardContent>
</Card>
))}
</div>
)} )}
{analytics && ( {analytics && (
@@ -313,40 +293,30 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
</Badge> </Badge>
)} )}
</h2> </h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <MetricsGrid
<div className="text-center"> metrics={[
<div className="text-xl font-bold text-[var(--primary)]"> {
{analytics.project.totalIssues} title: 'Tickets',
</div> value: analytics.project.totalIssues,
<div className="text-xs text-[var(--muted-foreground)]"> color: 'primary'
Tickets },
</div> {
</div> title: 'Équipe',
<div className="text-center"> value: analytics.teamMetrics.totalAssignees,
<div className="text-xl font-bold text-blue-500"> color: 'default'
{analytics.teamMetrics.totalAssignees} },
</div> {
<div className="text-xs text-[var(--muted-foreground)]"> title: 'Actifs',
Équipe value: analytics.teamMetrics.activeAssignees,
</div> color: 'success'
</div> },
<div className="text-center"> {
<div className="text-xl font-bold text-green-500"> title: 'Points',
{analytics.teamMetrics.activeAssignees} value: analytics.velocityMetrics.currentSprintPoints,
</div> color: 'warning'
<div className="text-xs text-[var(--muted-foreground)]"> }
Actifs ]}
</div> />
</div>
<div className="text-center">
<div className="text-xl font-bold text-orange-500">
{analytics.velocityMetrics.currentSprintPoints}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Points
</div>
</div>
</div>
</div> </div>
</CardHeader> </CardHeader>
</Card> </Card>
@@ -363,28 +333,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
<AnomalyDetectionPanel /> <AnomalyDetectionPanel />
{/* Onglets de navigation */} {/* Onglets de navigation */}
<div className="border-b border-[var(--border)]"> <Tabs
<nav className="flex space-x-8"> items={[
{[
{ id: 'overview', label: '📊 Vue d\'ensemble' }, { id: 'overview', label: '📊 Vue d\'ensemble' },
{ id: 'velocity', label: '🚀 Vélocité & Sprints' }, { id: 'velocity', label: '🚀 Vélocité & Sprints' },
{ id: 'analytics', label: '📈 Analytics avancées' }, { id: 'analytics', label: '📈 Analytics avancées' },
{ id: 'quality', label: '🎯 Qualité & Collaboration' } { id: 'quality', label: '🎯 Qualité & Collaboration' }
].map(tab => ( ]}
<button activeTab={activeTab}
key={tab.id} onTabChange={(tabId) => setActiveTab(tabId as 'overview' | 'velocity' | 'analytics' | 'quality')}
onClick={() => setActiveTab(tab.id as 'overview' | 'velocity' | 'analytics' | 'quality')} />
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.id
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:border-[var(--border)]'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Contenu des onglets */} {/* Contenu des onglets */}
{activeTab === 'overview' && ( {activeTab === 'overview' && (

View File

@@ -8,7 +8,7 @@ import { Alert as ShadcnAlert, AlertTitle, AlertDescription } from '@/components
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { StyledCard } from '@/components/ui/StyledCard'; import { StyledCard } from '@/components/ui/StyledCard';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone, Tabs, PriorityBadge, AchievementCard, ChallengeCard } from '@/components/ui'; import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone, Tabs, PriorityBadge, AchievementCard, ChallengeCard, PeriodSelector, SkeletonCard, SkeletonGrid, MetricsGrid } from '@/components/ui';
import { CheckboxItem, CheckboxItemData } from '@/components/ui/CheckboxItem'; import { CheckboxItem, CheckboxItemData } from '@/components/ui/CheckboxItem';
import { Calendar } from '@/components/ui/Calendar'; import { Calendar } from '@/components/ui/Calendar';
import { DailyAddForm } from '@/components/ui/DailyAddForm'; import { DailyAddForm } from '@/components/ui/DailyAddForm';
@@ -22,6 +22,7 @@ import { ChallengeData } from '@/components/ui/ChallengeCard';
export function UIShowcaseClient() { export function UIShowcaseClient() {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [selectedDate, setSelectedDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(new Date());
const [selectedPeriod, setSelectedPeriod] = useState('7d');
const [checkboxItems, setCheckboxItems] = useState<CheckboxItemData[]>([ const [checkboxItems, setCheckboxItems] = useState<CheckboxItemData[]>([
{ id: '1', text: 'Tâche complétée', isChecked: true, type: 'task' }, { id: '1', text: 'Tâche complétée', isChecked: true, type: 'task' },
{ id: '2', text: 'Réunion importante', isChecked: false, type: 'meeting' }, { id: '2', text: 'Réunion importante', isChecked: false, type: 'meeting' },
@@ -1081,6 +1082,93 @@ export function UIShowcaseClient() {
</div> </div>
</section> </section>
{/* Jira Dashboard Components Section */}
<section className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
Jira Dashboard Components
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* PeriodSelector */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">PeriodSelector</h3>
<div className="space-y-4">
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
PeriodSelector - Sélecteur de période
</div>
<PeriodSelector
options={[
{ value: '7d', label: '7j' },
{ value: '30d', label: '30j' },
{ value: '3m', label: '3m' },
{ value: 'current', label: 'Sprint' }
]}
selectedValue={selectedPeriod}
onValueChange={setSelectedPeriod}
/>
<div className="text-xs text-[var(--muted-foreground)]">
Période sélectionnée: {selectedPeriod}
</div>
</div>
</div>
</div>
{/* MetricsGrid */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">MetricsGrid</h3>
<div className="space-y-4">
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
MetricsGrid - Grille de métriques
</div>
<Card>
<CardContent className="p-4">
<MetricsGrid
metrics={[
{ title: 'Tickets', value: 42, color: 'primary' },
{ title: 'Équipe', value: 8, color: 'default' },
{ title: 'Actifs', value: 6, color: 'success' },
{ title: 'Points', value: 156, color: 'warning' }
]}
/>
</CardContent>
</Card>
</div>
</div>
</div>
{/* SkeletonCard */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">SkeletonCard</h3>
<div className="space-y-4">
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
SkeletonCard - Carte de chargement
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<SkeletonCard lines={3} />
<SkeletonCard lines={4} />
</div>
</div>
</div>
</div>
{/* SkeletonGrid */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-[var(--foreground)]">SkeletonGrid</h3>
<div className="space-y-4">
<div className="space-y-2">
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
SkeletonGrid - Grille de chargement
</div>
<SkeletonGrid count={4} lines={3} />
</div>
</div>
</div>
</div>
</section>
{/* Daily Components Section */} {/* Daily Components Section */}
<section className="space-y-8"> <section className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3"> <h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">

View File

@@ -0,0 +1,57 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
interface MetricsGridProps {
metrics: Array<{
title: string;
value: string | number;
subtitle?: string;
icon?: ReactNode;
color?: 'default' | 'primary' | 'success' | 'warning' | 'destructive';
}>;
className?: string;
columns?: 2 | 3 | 4;
}
export function MetricsGrid({
metrics,
className,
columns = 4
}: MetricsGridProps) {
const gridCols = {
2: 'grid-cols-2',
3: 'grid-cols-3',
4: 'grid-cols-2 lg:grid-cols-4'
};
return (
<div className={cn(
"grid gap-4",
gridCols[columns],
className
)}>
{metrics.map((metric, index) => (
<div key={index} className="text-center">
<div className={cn(
"text-xl font-bold",
metric.color === 'primary' && 'text-[var(--primary)]',
metric.color === 'success' && 'text-[var(--success)]',
metric.color === 'warning' && 'text-[var(--accent)]',
metric.color === 'destructive' && 'text-[var(--destructive)]',
!metric.color && 'text-[var(--foreground)]'
)}>
{metric.value}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
{metric.title}
</div>
{metric.subtitle && (
<div className="text-xs text-[var(--muted-foreground)] mt-1">
{metric.subtitle}
</div>
)}
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,45 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
export interface PeriodOption {
value: string;
label: string;
icon?: ReactNode;
}
interface PeriodSelectorProps {
options: PeriodOption[];
selectedValue: string;
onValueChange: (value: string) => void;
className?: string;
}
export function PeriodSelector({
options,
selectedValue,
onValueChange,
className
}: PeriodSelectorProps) {
return (
<div className={cn(
"flex bg-[var(--card)] border border-[var(--border)] rounded-lg p-1",
className
)}>
{options.map((option) => (
<button
key={option.value}
onClick={() => onValueChange(option.value)}
className={cn(
"px-3 py-1 text-sm rounded transition-all flex items-center gap-1",
selectedValue === option.value
? 'bg-[var(--primary)] text-[var(--primary-foreground)]'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
)}
>
{option.icon && <span>{option.icon}</span>}
<span>{option.label}</span>
</button>
))}
</div>
);
}

View File

@@ -0,0 +1,50 @@
import { Card, CardContent } from './Card';
import { cn } from '@/lib/utils';
interface SkeletonCardProps {
className?: string;
lines?: number;
}
export function SkeletonCard({ className, lines = 3 }: SkeletonCardProps) {
return (
<Card className={cn("animate-pulse", className)}>
<CardContent className="p-6">
<div className="space-y-4">
{/* Titre */}
<div className="h-4 bg-[var(--muted)] rounded mb-4"></div>
{/* Valeur principale */}
<div className="h-8 bg-[var(--muted)] rounded mb-2"></div>
{/* Lignes supplémentaires */}
{Array.from({ length: lines - 2 }).map((_, i) => (
<div
key={i}
className={cn(
"h-4 bg-[var(--muted)] rounded",
i === lines - 3 ? "w-2/3" : "w-full"
)}
></div>
))}
</div>
</CardContent>
</Card>
);
}
interface SkeletonGridProps {
count?: number;
className?: string;
lines?: number;
}
export function SkeletonGrid({ count = 6, className, lines = 3 }: SkeletonGridProps) {
return (
<div className={cn("grid grid-cols-1 lg:grid-cols-3 gap-6", className)}>
{Array.from({ length: count }).map((_, i) => (
<SkeletonCard key={i} lines={lines} />
))}
</div>
);
}

View File

@@ -35,6 +35,11 @@ export { DailyAddForm } from './DailyAddForm';
export { AlertBanner } from './AlertBanner'; export { AlertBanner } from './AlertBanner';
export { CollapsibleSection } from './CollapsibleSection'; export { CollapsibleSection } from './CollapsibleSection';
// Composants Jira Dashboard
export { PeriodSelector } from './PeriodSelector';
export { SkeletonCard, SkeletonGrid } from './SkeletonCard';
export { MetricsGrid } from './MetricsGrid';
// Composants existants // Composants existants
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card'; export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
export { FontSizeToggle } from './FontSizeToggle'; export { FontSizeToggle } from './FontSizeToggle';