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:
@@ -9,6 +9,9 @@ import { Header } from '@/components/ui/Header';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
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 { TeamDistributionChart } from '@/components/jira/TeamDistributionChart';
|
||||
import { CycleTimeChart } from '@/components/jira/CycleTimeChart';
|
||||
@@ -197,26 +200,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 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: '30d', label: '30j' },
|
||||
{ value: '3m', label: '3m' },
|
||||
{ value: 'current', label: 'Sprint' }
|
||||
].map((period: { value: string; label: string }) => (
|
||||
<button
|
||||
key={period.value}
|
||||
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>
|
||||
]}
|
||||
selectedValue={selectedPeriod}
|
||||
onValueChange={(value) => setSelectedPeriod(value as PeriodFilter)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{analytics && (
|
||||
@@ -260,40 +253,27 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
||||
|
||||
{/* Contenu principal */}
|
||||
{error && (
|
||||
<Card className="mb-6 border-red-500/20 bg-red-500/10">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2" style={{ color: 'var(--destructive)' }}>
|
||||
<span>❌</span>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AlertBanner
|
||||
title="Erreur"
|
||||
items={[{ id: 'error', title: error }]}
|
||||
icon="❌"
|
||||
variant="error"
|
||||
className="mb-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
{exportError && (
|
||||
<Card className="mb-6 border-orange-500/20 bg-orange-500/10">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2" style={{ color: 'var(--accent)' }}>
|
||||
<span>⚠️</span>
|
||||
<span>Erreur d'export: {exportError}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AlertBanner
|
||||
title="Erreur d'export"
|
||||
items={[{ id: 'export-error', title: exportError }]}
|
||||
icon="⚠️"
|
||||
variant="warning"
|
||||
className="mb-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoading && !analytics && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-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>
|
||||
<SkeletonGrid count={6} />
|
||||
)}
|
||||
|
||||
{analytics && (
|
||||
@@ -313,40 +293,30 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
||||
</Badge>
|
||||
)}
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-[var(--primary)]">
|
||||
{analytics.project.totalIssues}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)]">
|
||||
Tickets
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-blue-500">
|
||||
{analytics.teamMetrics.totalAssignees}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)]">
|
||||
Équipe
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-xl font-bold text-green-500">
|
||||
{analytics.teamMetrics.activeAssignees}
|
||||
</div>
|
||||
<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>
|
||||
<MetricsGrid
|
||||
metrics={[
|
||||
{
|
||||
title: 'Tickets',
|
||||
value: analytics.project.totalIssues,
|
||||
color: 'primary'
|
||||
},
|
||||
{
|
||||
title: 'Équipe',
|
||||
value: analytics.teamMetrics.totalAssignees,
|
||||
color: 'default'
|
||||
},
|
||||
{
|
||||
title: 'Actifs',
|
||||
value: analytics.teamMetrics.activeAssignees,
|
||||
color: 'success'
|
||||
},
|
||||
{
|
||||
title: 'Points',
|
||||
value: analytics.velocityMetrics.currentSprintPoints,
|
||||
color: 'warning'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -363,28 +333,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
|
||||
<AnomalyDetectionPanel />
|
||||
|
||||
{/* Onglets de navigation */}
|
||||
<div className="border-b border-[var(--border)]">
|
||||
<nav className="flex space-x-8">
|
||||
{[
|
||||
{ id: 'overview', label: '📊 Vue d\'ensemble' },
|
||||
{ id: 'velocity', label: '🚀 Vélocité & Sprints' },
|
||||
{ id: 'analytics', label: '📈 Analytics avancées' },
|
||||
{ id: 'quality', label: '🎯 Qualité & Collaboration' }
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
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>
|
||||
<Tabs
|
||||
items={[
|
||||
{ id: 'overview', label: '📊 Vue d\'ensemble' },
|
||||
{ id: 'velocity', label: '🚀 Vélocité & Sprints' },
|
||||
{ id: 'analytics', label: '📈 Analytics avancées' },
|
||||
{ id: 'quality', label: '🎯 Qualité & Collaboration' }
|
||||
]}
|
||||
activeTab={activeTab}
|
||||
onTabChange={(tabId) => setActiveTab(tabId as 'overview' | 'velocity' | 'analytics' | 'quality')}
|
||||
/>
|
||||
|
||||
{/* Contenu des onglets */}
|
||||
{activeTab === 'overview' && (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Alert as ShadcnAlert, AlertTitle, AlertDescription } from '@/components
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { StyledCard } from '@/components/ui/StyledCard';
|
||||
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 { Calendar } from '@/components/ui/Calendar';
|
||||
import { DailyAddForm } from '@/components/ui/DailyAddForm';
|
||||
@@ -22,6 +22,7 @@ import { ChallengeData } from '@/components/ui/ChallengeCard';
|
||||
export function UIShowcaseClient() {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('7d');
|
||||
const [checkboxItems, setCheckboxItems] = useState<CheckboxItemData[]>([
|
||||
{ id: '1', text: 'Tâche complétée', isChecked: true, type: 'task' },
|
||||
{ id: '2', text: 'Réunion importante', isChecked: false, type: 'meeting' },
|
||||
@@ -1081,6 +1082,93 @@ export function UIShowcaseClient() {
|
||||
</div>
|
||||
</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 */}
|
||||
<section className="space-y-8">
|
||||
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||
|
||||
57
src/components/ui/MetricsGrid.tsx
Normal file
57
src/components/ui/MetricsGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
src/components/ui/PeriodSelector.tsx
Normal file
45
src/components/ui/PeriodSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
50
src/components/ui/SkeletonCard.tsx
Normal file
50
src/components/ui/SkeletonCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,11 @@ export { DailyAddForm } from './DailyAddForm';
|
||||
export { AlertBanner } from './AlertBanner';
|
||||
export { CollapsibleSection } from './CollapsibleSection';
|
||||
|
||||
// Composants Jira Dashboard
|
||||
export { PeriodSelector } from './PeriodSelector';
|
||||
export { SkeletonCard, SkeletonGrid } from './SkeletonCard';
|
||||
export { MetricsGrid } from './MetricsGrid';
|
||||
|
||||
// Composants existants
|
||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
||||
export { FontSizeToggle } from './FontSizeToggle';
|
||||
|
||||
Reference in New Issue
Block a user