- 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`.
152 lines
5.9 KiB
TypeScript
152 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import { AssigneeWorkload, StatusDistribution } from '@/lib/types';
|
|
|
|
interface TeamActivityHeatmapProps {
|
|
workloadByAssignee: AssigneeWorkload[];
|
|
statusDistribution: StatusDistribution[];
|
|
className?: string;
|
|
}
|
|
|
|
export function TeamActivityHeatmap({ workloadByAssignee, statusDistribution, className }: TeamActivityHeatmapProps) {
|
|
// Calculer l'intensité maximale pour la normalisation
|
|
const maxWorkload = Math.max(...workloadByAssignee.map(a => a.totalActive));
|
|
|
|
// Fonction pour calculer l'intensité de couleur
|
|
const getIntensity = (value: number) => {
|
|
if (maxWorkload === 0) return 0;
|
|
return (value / maxWorkload) * 100;
|
|
};
|
|
|
|
// Couleurs pour les différents types de travail
|
|
const getWorkloadColor = (todo: number, inProgress: number, review: number) => {
|
|
const total = todo + inProgress + review;
|
|
if (total === 0) return 'bg-[var(--muted)]/20';
|
|
|
|
// Dominante par type de travail
|
|
if (review > inProgress && review > todo) {
|
|
return 'bg-purple-500'; // Review dominant
|
|
} else if (inProgress > todo) {
|
|
return 'bg-orange-500'; // In Progress dominant
|
|
} else {
|
|
return 'bg-blue-500'; // Todo dominant
|
|
}
|
|
};
|
|
|
|
const getOpacity = (total: number) => {
|
|
const intensity = getIntensity(total);
|
|
return Math.max(0.2, intensity / 100);
|
|
};
|
|
|
|
return (
|
|
<div className={className}>
|
|
<div className="space-y-4">
|
|
{/* Heatmap des assignees */}
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-3">Intensité de travail par membre</h4>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
|
|
{workloadByAssignee.map(assignee => (
|
|
<div
|
|
key={assignee.assignee}
|
|
className="relative p-3 rounded-lg border border-[var(--border)] transition-all hover:scale-105"
|
|
style={{
|
|
backgroundColor: getWorkloadColor(assignee.todoCount, assignee.inProgressCount, assignee.reviewCount),
|
|
opacity: getOpacity(assignee.totalActive)
|
|
}}
|
|
>
|
|
<div className="text-white text-xs font-medium mb-1 truncate">
|
|
{assignee.displayName}
|
|
</div>
|
|
<div className="text-white text-lg font-bold">
|
|
{assignee.totalActive}
|
|
</div>
|
|
<div className="text-white/80 text-xs">
|
|
{assignee.todoCount > 0 && `${assignee.todoCount} à faire`}
|
|
{assignee.inProgressCount > 0 && ` ${assignee.inProgressCount} en cours`}
|
|
{assignee.reviewCount > 0 && ` ${assignee.reviewCount} review`}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Légende */}
|
|
<div className="flex items-center gap-4 text-xs">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-blue-500 rounded"></div>
|
|
<span>À faire dominant</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-orange-500 rounded"></div>
|
|
<span>En cours dominant</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 bg-purple-500 rounded"></div>
|
|
<span>Review dominant</span>
|
|
</div>
|
|
<div className="text-[var(--muted-foreground)]">
|
|
(Opacité = charge de travail)
|
|
</div>
|
|
</div>
|
|
|
|
{/* Matrice de statuts */}
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-3">Répartition globale par statut</h4>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
{statusDistribution.slice(0, 8).map(status => {
|
|
const intensity = (status.count / Math.max(...statusDistribution.map(s => s.count))) * 100;
|
|
return (
|
|
<div
|
|
key={status.status}
|
|
className="p-3 rounded-lg border border-[var(--border)] bg-gradient-to-r from-[var(--primary)]/20 to-[var(--primary)]"
|
|
style={{
|
|
backgroundImage: `linear-gradient(to right, var(--primary) ${intensity}%, transparent ${intensity}%)`
|
|
}}
|
|
>
|
|
<div className="text-[var(--foreground)] text-xs font-medium mb-1 truncate">
|
|
{status.status}
|
|
</div>
|
|
<div className="text-[var(--foreground)] text-lg font-bold">
|
|
{status.count}
|
|
</div>
|
|
<div className="text-[var(--foreground)]/80 text-xs">
|
|
{status.percentage}%
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Métriques rapides */}
|
|
<div className="grid grid-cols-3 gap-4 pt-4 border-t border-[var(--border)]">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-500">
|
|
{workloadByAssignee.filter(a => a.totalActive > 0).length}
|
|
</div>
|
|
<div className="text-xs text-[var(--muted-foreground)]">
|
|
Membres actifs
|
|
</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-orange-500">
|
|
{workloadByAssignee.reduce((sum, a) => sum + a.totalActive, 0)}
|
|
</div>
|
|
<div className="text-xs text-[var(--muted-foreground)]">
|
|
Total WIP
|
|
</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-500">
|
|
{Math.round(workloadByAssignee.reduce((sum, a) => sum + a.totalActive, 0) / Math.max(1, workloadByAssignee.filter(a => a.totalActive > 0).length) * 10) / 10}
|
|
</div>
|
|
<div className="text-xs text-[var(--muted-foreground)]">
|
|
WIP moyen/membre
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|