diff --git a/frontend/app/insights/analytics/page.tsx b/frontend/app/insights/analytics/page.tsx index 1082a20..875f6ed 100644 --- a/frontend/app/insights/analytics/page.tsx +++ b/frontend/app/insights/analytics/page.tsx @@ -16,19 +16,129 @@ import TrendMetricCard from "@/app/components/analytics-comp/TrendMetricCard"; const sprintData: Record = { "Sprint 42": [ - { id: "alex", name: "Alex Rivera", role: "Lead Frontend Engineer", assigned: 85, completed: 72, reviews: 18, focus: "Frontend", activity: [30, 50, 80, 100, 90, 60, 20, 85], image: "https://i.pravatar.cc/96?img=11" }, - { id: "jordan", name: "Jordan Smith", role: "Backend Engineer", assigned: 60, completed: 58, reviews: 15, focus: "API", activity: [45, 65, 55, 80, 70, 64, 58, 75], image: "https://i.pravatar.cc/96?img=12" }, - { id: "casey", name: "Casey Morgan", role: "Fullstack Developer", assigned: 95, completed: 90, reviews: 24, focus: "Fullstack", activity: [70, 40, 90, 100, 50, 80, 30, 95], image: "https://i.pravatar.cc/96?img=13" }, - { id: "riley", name: "Riley Lee", role: "QA Analyst", assigned: 45, completed: 45, reviews: 11, focus: "QA", activity: [35, 55, 42, 62, 78, 45, 68, 72], image: "https://i.pravatar.cc/96?img=14" }, - { id: "morgan", name: "Morgan Patel", role: "Product Engineer", assigned: 80, completed: 40, reviews: 9, focus: "Product", activity: [25, 35, 52, 48, 60, 42, 38, 50], image: "https://i.pravatar.cc/96?img=15" }, - { id: "quinn", name: "Quinn Taylor", role: "DevOps Engineer", assigned: 70, completed: 68, reviews: 14, focus: "Ops", activity: [55, 68, 72, 75, 62, 74, 80, 78], image: "https://i.pravatar.cc/96?img=16" }, + { + id: "alex", + name: "Alex Rivera", + role: "Lead Frontend Engineer", + assigned: 85, + completed: 72, + reviews: 18, + focus: "Frontend", + activity: [30, 50, 80, 100, 90, 60, 20, 85], + image: "https://i.pravatar.cc/96?img=11", + }, + { + id: "jordan", + name: "Jordan Smith", + role: "Backend Engineer", + assigned: 60, + completed: 58, + reviews: 15, + focus: "API", + activity: [45, 65, 55, 80, 70, 64, 58, 75], + image: "https://i.pravatar.cc/96?img=12", + }, + { + id: "casey", + name: "Casey Morgan", + role: "Fullstack Developer", + assigned: 95, + completed: 90, + reviews: 24, + focus: "Fullstack", + activity: [70, 40, 90, 100, 50, 80, 30, 95], + image: "https://i.pravatar.cc/96?img=13", + }, + { + id: "riley", + name: "Riley Lee", + role: "QA Analyst", + assigned: 45, + completed: 45, + reviews: 11, + focus: "QA", + activity: [35, 55, 42, 62, 78, 45, 68, 72], + image: "https://i.pravatar.cc/96?img=14", + }, + { + id: "morgan", + name: "Morgan Patel", + role: "Product Engineer", + assigned: 80, + completed: 40, + reviews: 9, + focus: "Product", + activity: [25, 35, 52, 48, 60, 42, 38, 50], + image: "https://i.pravatar.cc/96?img=15", + }, + { + id: "quinn", + name: "Quinn Taylor", + role: "DevOps Engineer", + assigned: 70, + completed: 68, + reviews: 14, + focus: "Ops", + activity: [55, 68, 72, 75, 62, 74, 80, 78], + image: "https://i.pravatar.cc/96?img=16", + }, ], "Sprint 41": [ - { id: "alex", name: "Alex Rivera", role: "Lead Frontend Engineer", assigned: 76, completed: 70, reviews: 21, focus: "Frontend", activity: [44, 58, 62, 88, 92, 70, 55, 81], image: "https://i.pravatar.cc/96?img=11" }, - { id: "jordan", name: "Jordan Smith", role: "Backend Engineer", assigned: 72, completed: 61, reviews: 12, focus: "API", activity: [40, 48, 65, 72, 68, 71, 52, 64], image: "https://i.pravatar.cc/96?img=12" }, - { id: "casey", name: "Casey Morgan", role: "Fullstack Developer", assigned: 88, completed: 84, reviews: 20, focus: "Fullstack", activity: [58, 74, 82, 95, 76, 86, 62, 91], image: "https://i.pravatar.cc/96?img=13" }, - { id: "riley", name: "Riley Lee", role: "QA Analyst", assigned: 52, completed: 49, reviews: 16, focus: "QA", activity: [62, 60, 70, 66, 78, 74, 72, 80], image: "https://i.pravatar.cc/96?img=14" }, - { id: "morgan", name: "Morgan Patel", role: "Product Engineer", assigned: 66, completed: 59, reviews: 10, focus: "Product", activity: [32, 46, 58, 62, 60, 55, 64, 68], image: "https://i.pravatar.cc/96?img=15" }, + { + id: "alex", + name: "Alex Rivera", + role: "Lead Frontend Engineer", + assigned: 76, + completed: 70, + reviews: 21, + focus: "Frontend", + activity: [44, 58, 62, 88, 92, 70, 55, 81], + image: "https://i.pravatar.cc/96?img=11", + }, + { + id: "jordan", + name: "Jordan Smith", + role: "Backend Engineer", + assigned: 72, + completed: 61, + reviews: 12, + focus: "API", + activity: [40, 48, 65, 72, 68, 71, 52, 64], + image: "https://i.pravatar.cc/96?img=12", + }, + { + id: "casey", + name: "Casey Morgan", + role: "Fullstack Developer", + assigned: 88, + completed: 84, + reviews: 20, + focus: "Fullstack", + activity: [58, 74, 82, 95, 76, 86, 62, 91], + image: "https://i.pravatar.cc/96?img=13", + }, + { + id: "riley", + name: "Riley Lee", + role: "QA Analyst", + assigned: 52, + completed: 49, + reviews: 16, + focus: "QA", + activity: [62, 60, 70, 66, 78, 74, 72, 80], + image: "https://i.pravatar.cc/96?img=14", + }, + { + id: "morgan", + name: "Morgan Patel", + role: "Product Engineer", + assigned: 66, + completed: 59, + reviews: 10, + focus: "Product", + activity: [32, 46, 58, 62, 60, 55, 64, 68], + image: "https://i.pravatar.cc/96?img=15", + }, ], }; @@ -49,6 +159,9 @@ export default function InsightsAnalyticsPage() { const [selectedMemberId, setSelectedMemberId] = useState("casey"); const [query, setQuery] = useState(""); const [sortKey, setSortKey] = useState("score"); + const [comparisonSort, setComparisonSort] = useState< + "improvement" | "decline" | "name" + >("improvement"); const [isLoading, setIsLoading] = useState(false); const [loadError, setLoadError] = useState(""); @@ -70,18 +183,17 @@ export default function InsightsAnalyticsPage() { }); if (!response.ok) throw new Error("Failed to load analytics"); const body = (await response.json()) as AnalyticsResponse; - const nextData = body.sprints.reduce>( - (acc, item) => { - acc[item.label] = item.members; - return acc; - }, - {} - ); + const nextData = body.sprints.reduce< + Record + >((acc, item) => { + acc[item.label] = item.members; + return acc; + }, {}); if (Object.keys(nextData).length > 0) { setData(nextData); const firstSprint = Object.keys(nextData)[0]; - setSprintA((current) => nextData[current] ? current : firstSprint ); + setSprintA((current) => (nextData[current] ? current : firstSprint)); setSelectedMemberId((current) => { const activeMembers = nextData[firstSprint] || []; return activeMembers.some((member) => member.id === current) @@ -91,7 +203,9 @@ export default function InsightsAnalyticsPage() { } } catch (error) { if ((error as Error).name !== "AbortError") { - setLoadError("Showing local analytics because the backend is unavailable."); + setLoadError( + "Showing local analytics because the backend is unavailable.", + ); } } finally { setIsLoading(false); @@ -104,66 +218,83 @@ export default function InsightsAnalyticsPage() { const members = useMemo( () => data[sprintA] || Object.values(data)[0] || [], - [data, sprintA] + [data, sprintA], ); - const compareMembers = useMemo( - () => data[sprintB] || [], - [data, sprintB] -); - -const sprintAMetrics = { - completed: members.reduce( - (sum, member) => sum + member.completed, - 0 - ), - reviews: members.reduce( - (sum, member) => sum + member.reviews, - 0 - ), -}; + const compareMembers = useMemo(() => data[sprintB] || [], [data, sprintB]); -const sprintBMetrics = { - completed: compareMembers.reduce( - (sum, member) => sum + member.completed, - 0 - ), - reviews: compareMembers.reduce( - (sum, member) => sum + member.reviews, - 0 - ), -}; + const sprintAMetrics = { + completed: members.reduce((sum, member) => sum + member.completed, 0), + reviews: members.reduce((sum, member) => sum + member.reviews, 0), + }; + + const sprintBMetrics = { + completed: compareMembers.reduce( + (sum, member) => sum + member.completed, + 0, + ), + reviews: compareMembers.reduce((sum, member) => sum + member.reviews, 0), + }; + const contributorComparison = members + .map((member) => { + const previousMember = compareMembers.find( + (item) => item.id === member.id, + ); + const previousCompleted = previousMember?.completed ?? 0; - const selectedMember = members.find((member) => member.id === selectedMemberId) ?? members[0]; + const change = member.completed - previousCompleted; + + return { + id: member.id, + name: member.name, + current: member.completed, + previous: previousCompleted, + change, + }; + }) + .sort((a, b) => { + if (comparisonSort === "improvement") { + return b.change - a.change; + } + + if (comparisonSort === "decline") { + return a.change - b.change; + } + + return a.name.localeCompare(b.name); + }); + + const selectedMember = + members.find((member) => member.id === selectedMemberId) ?? members[0]; const filteredMembers = useMemo(() => { const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) return members; return members.filter((member) => [member.name, member.role, member.focus].some((value) => - value.toLowerCase().includes(normalizedQuery) - ) + value.toLowerCase().includes(normalizedQuery), + ), ); }, [members, query]); return ( - <> - { - setSprintA(nextSprint); - setSelectedMemberId(data[nextSprint][0]?.id || ""); - }} - onSprintBChange={(nextSprint) => { - setSprintB(nextSprint); - }} - /> - -
+ <> + { + setSprintA(nextSprint); + setSelectedMemberId(data[nextSprint][0]?.id || ""); + }} + onSprintBChange={(nextSprint) => { + setSprintB(nextSprint); + }} + /> + +
{(isLoading || loadError) && (
@@ -171,26 +302,107 @@ const sprintBMetrics = {
)}
-

- Sprint Comparison -

- -
- - - - - -
-
+

Sprint Comparison

+ +
+ + + +
+
+ +
+
+

Contributor Comparison

+ + +
+ +
+ + + + + + + + + + + + {contributorComparison.length === 0 ? ( + + + + ) : ( + contributorComparison.map((member) => { + const trend = + member.change > 0 ? "▲" : member.change < 0 ? "▼" : "●"; + + const colorClass = + member.change > 0 + ? "text-green-600" + : member.change < 0 + ? "text-red-600" + : "text-gray-500"; + + return ( + + + + + + + + + + ); + }) + )} + +
Contributor{sprintA}{sprintB}Change
+ No contributor comparison data available for the + selected sprints. +
+ {member.name} + + {member.current} + + {member.previous} + + {trend} {member.change > 0 ? "+" : ""} + {member.change} +
+
+
+