- Introduced `projectKey` and `ignoredProjects` fields in Jira configuration to enhance analytics capabilities. - Implemented project validation logic in `JiraConfigClient` and integrated it into the `JiraConfigForm` for user input. - Updated `IntegrationsSettingsPageClient` to display analytics dashboard link based on the configured project key. - Enhanced API routes to handle project key in Jira sync and user preferences. - Marked related tasks as complete in `TODO.md`.
127 lines
4.1 KiB
TypeScript
127 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
|
|
import { AssigneeDistribution } from '@/lib/types';
|
|
|
|
interface TeamDistributionChartProps {
|
|
distribution: AssigneeDistribution[];
|
|
className?: string;
|
|
}
|
|
|
|
const COLORS = [
|
|
'hsl(142, 76%, 36%)', // Green
|
|
'hsl(217, 91%, 60%)', // Blue
|
|
'hsl(45, 93%, 47%)', // Yellow
|
|
'hsl(0, 84%, 60%)', // Red
|
|
'hsl(262, 83%, 58%)', // Purple
|
|
'hsl(319, 70%, 52%)', // Pink
|
|
'hsl(173, 80%, 40%)', // Teal
|
|
'hsl(27, 96%, 61%)', // Orange
|
|
];
|
|
|
|
export function TeamDistributionChart({ distribution, className }: TeamDistributionChartProps) {
|
|
// Préparer les données pour le graphique (top 8 membres)
|
|
const chartData = distribution.slice(0, 8).map((assignee, index) => ({
|
|
name: assignee.displayName,
|
|
value: assignee.totalIssues,
|
|
completed: assignee.completedIssues,
|
|
inProgress: assignee.inProgressIssues,
|
|
percentage: assignee.percentage,
|
|
color: COLORS[index % COLORS.length]
|
|
}));
|
|
|
|
const CustomTooltip = ({ active, payload }: {
|
|
active?: boolean;
|
|
payload?: Array<{ payload: { name: string; value: number; completed: number; inProgress: number; percentage: number } }>
|
|
}) => {
|
|
if (active && payload && payload.length) {
|
|
const data = payload[0].payload;
|
|
return (
|
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
|
|
<p className="font-medium text-sm mb-2">{data.name}</p>
|
|
<div className="space-y-1 text-xs">
|
|
<div className="flex justify-between gap-4">
|
|
<span>Total:</span>
|
|
<span className="font-mono text-[var(--primary)]">{data.value} tickets</span>
|
|
</div>
|
|
<div className="flex justify-between gap-4">
|
|
<span>Complétés:</span>
|
|
<span className="font-mono text-green-500">{data.completed}</span>
|
|
</div>
|
|
<div className="flex justify-between gap-4">
|
|
<span>En cours:</span>
|
|
<span className="font-mono text-orange-500">{data.inProgress}</span>
|
|
</div>
|
|
<div className="flex justify-between gap-4">
|
|
<span>Part équipe:</span>
|
|
<span className="font-mono text-blue-500">{data.percentage}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const CustomLabel = (props: any) => {
|
|
const { cx, cy, midAngle, innerRadius, outerRadius, percentage } = props;
|
|
if (percentage < 5) return null; // Ne pas afficher les labels pour les petites sections
|
|
|
|
const RADIAN = Math.PI / 180;
|
|
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
|
const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
|
const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
|
|
|
return (
|
|
<text
|
|
x={x}
|
|
y={y}
|
|
fill="white"
|
|
textAnchor={x > cx ? 'start' : 'end'}
|
|
dominantBaseline="central"
|
|
fontSize="12"
|
|
fontWeight="500"
|
|
>
|
|
{`${percentage}%`}
|
|
</text>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className={className}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={chartData}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={false}
|
|
label={CustomLabel}
|
|
outerRadius={80}
|
|
fill="#8884d8"
|
|
dataKey="value"
|
|
>
|
|
{chartData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Legend
|
|
wrapperStyle={{
|
|
fontSize: '12px',
|
|
color: 'var(--muted-foreground)'
|
|
}}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
formatter={(value: any, entry: any) => (
|
|
<span style={{ color: entry.color }}>
|
|
{value} ({entry.payload.value})
|
|
</span>
|
|
)}
|
|
/>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
}
|