diff --git a/src/components/Analytics/AnalyticsDashboard.jsx b/src/components/Analytics/AnalyticsDashboard.jsx
new file mode 100644
index 0000000000..abd1ca0d2e
--- /dev/null
+++ b/src/components/Analytics/AnalyticsDashboard.jsx
@@ -0,0 +1,54 @@
+import { useState } from 'react';
+import { useSelector } from 'react-redux';
+import ReviewersRequirementChart from './ReviewersRequirementChart';
+import styles from './AnalyticsDashboard.module.css';
+
+const durationOptions = [
+ { label: 'Last Week', value: 'lastWeek' },
+ { label: 'Last 2 weeks', value: 'last2weeks' },
+ { label: 'Last Month', value: 'lastMonth' },
+ { label: 'All Time', value: 'allTime' },
+];
+
+const AnalyticsDashboard = () => {
+ const darkMode = useSelector(state => state.theme.darkMode);
+ const [duration, setDuration] = useState('lastWeek');
+
+ return (
+
+
+
Reviewers Ranked by Requirement Satisfied
+
+
+
+ Duration:
+
+ setDuration(e.target.value)}
+ >
+ {durationOptions.map(opt => (
+
+ {opt.label}
+
+ ))}
+
+
+
+
+
+ Shows how many PR reviews each reviewer completed in the selected period, grouped by review
+ quality. The list length comes from the backend for that duration (not a fixed frontend
+ limit).
+
+
+
+
+
+
+ );
+};
+
+export default AnalyticsDashboard;
diff --git a/src/components/Analytics/AnalyticsDashboard.module.css b/src/components/Analytics/AnalyticsDashboard.module.css
new file mode 100644
index 0000000000..2ff9f30254
--- /dev/null
+++ b/src/components/Analytics/AnalyticsDashboard.module.css
@@ -0,0 +1,129 @@
+.page {
+ width: 100%;
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 1.5rem 1.25rem 2.5rem;
+ box-sizing: border-box;
+ min-height: calc(100vh - 120px);
+}
+
+.header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1.25rem;
+}
+
+.title {
+ margin: 0;
+ font-size: clamp(1.25rem, 2.5vw, 1.75rem);
+ font-weight: 600;
+ line-height: 1.3;
+ color: #052c65;
+ flex: 1 1 280px;
+}
+
+.filterGroup {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex: 0 0 auto;
+}
+
+.filterLabel {
+ margin: 0;
+ font-weight: 500;
+ color: #1f2937;
+ white-space: nowrap;
+}
+
+.filterSelect {
+ min-width: 160px;
+ padding: 0.45rem 0.75rem;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ background: #fff;
+ color: #111827;
+ font-size: 0.95rem;
+}
+
+.chartCard {
+ width: 100%;
+ border: 1px solid #e5e7eb;
+ border-radius: 10px;
+ background: #fff;
+ padding: 1rem 0.75rem 1.25rem;
+ box-sizing: border-box;
+ overflow-x: auto;
+}
+
+.helperText {
+ margin: 0 0 1rem;
+ font-size: 0.9rem;
+ color: #4b5563;
+}
+
+.statusMessage {
+ display: grid;
+ place-items: center;
+ min-height: 220px;
+ margin: 0;
+ text-align: center;
+ color: #374151;
+ font-size: 1rem;
+}
+
+/* Dark mode */
+.pageDark {
+ color: #f8fafc;
+}
+
+.pageDark .title {
+ color: #e2e8f0;
+}
+
+.pageDark .filterLabel {
+ color: #e2e8f0;
+}
+
+.pageDark .filterSelect {
+ background: #2d3748;
+ border-color: #4a5568;
+ color: #f8fafc;
+}
+
+.pageDark .chartCard {
+ background: #1a202c;
+ border-color: #4a5568;
+}
+
+.pageDark .helperText,
+.pageDark .statusMessage {
+ color: #cbd5e1;
+}
+
+@media (width <= 768px) {
+ .page {
+ padding: 1rem 0.75rem 2rem;
+ }
+
+ .header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .filterGroup {
+ width: 100%;
+ }
+
+ .filterSelect {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .chartCard {
+ padding: 0.75rem 0.25rem 1rem;
+ }
+}
diff --git a/src/components/Analytics/PopularPRChart.jsx b/src/components/Analytics/PopularPRChart.jsx
new file mode 100644
index 0000000000..e037f3013d
--- /dev/null
+++ b/src/components/Analytics/PopularPRChart.jsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { Bar, BarChart, LabelList, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
+
+const data = [
+ { prNumber: 'PR1234', reviews: 10 },
+ { prNumber: 'PR1235', reviews: 8 },
+ { prNumber: 'PR1236', reviews: 15 },
+ { prNumber: 'PR1237', reviews: 5 },
+ { prNumber: 'PR1238', reviews: 12 },
+ { prNumber: 'PR1231', reviews: 12 },
+ { prNumber: 'PR1256', reviews: 17 },
+ { prNumber: 'PR1255', reviews: 19 },
+ { prNumber: 'PR1254', reviews: 1 },
+ { prNumber: 'PR1253', reviews: 2 },
+ { prNumber: 'PR1252', reviews: 7 },
+ { prNumber: 'PR1251', reviews: 9 },
+ { prNumber: 'PR1250', reviews: 11 },
+ { prNumber: 'PR1249', reviews: 19 },
+ { prNumber: 'PR1248', reviews: 0 },
+ { prNumber: 'PR1242', reviews: 2 },
+ { prNumber: 'PR1241', reviews: 50 },
+];
+
+const PopularPRChart = () => {
+ const filteredData = data;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PopularPRChart;
diff --git a/src/components/Analytics/ReviewersRequirementChart.jsx b/src/components/Analytics/ReviewersRequirementChart.jsx
new file mode 100644
index 0000000000..af46a50a8e
--- /dev/null
+++ b/src/components/Analytics/ReviewersRequirementChart.jsx
@@ -0,0 +1,239 @@
+import axios from 'axios';
+import { useEffect, useState } from 'react';
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ LabelList,
+ Legend,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+import { ENDPOINTS } from '../../utils/URL';
+import styles from './AnalyticsDashboard.module.css';
+
+const useYAxisWidth = () => {
+ const [yAxisWidth, setYAxisWidth] = useState(180);
+
+ useEffect(() => {
+ const updateWidth = () => {
+ setYAxisWidth(window.innerWidth < 768 ? 110 : 180);
+ };
+
+ updateWidth();
+ window.addEventListener('resize', updateWidth);
+ return () => window.removeEventListener('resize', updateWidth);
+ }, []);
+
+ return yAxisWidth;
+};
+
+const COLORS = {
+ exceptional: '#052C65',
+ sufficient: '#4682B4',
+ needsChanges: '#FF8C00',
+ didNotReview: '#A9A9A9',
+};
+
+const getCount = (counts, key) => counts?.[key] || 0;
+
+const normalizeReviewCounts = item => {
+ const counts = item.counts || {};
+ const exceptional = getCount(counts, 'Exceptional');
+ const sufficient = getCount(counts, 'Sufficient');
+ const needsChanges = getCount(counts, 'Needs Changes');
+ const didNotReview = getCount(counts, 'Did Not Review');
+
+ return {
+ reviewer: item.reviewer,
+ exceptional,
+ sufficient,
+ needsChanges,
+ didNotReview,
+ total: exceptional + sufficient + needsChanges + didNotReview,
+ };
+};
+
+const CustomTooltip = ({ active, payload, label, darkMode }) => {
+ if (!active || !payload?.length) return null;
+
+ const textColor = darkMode ? '#f8fafc' : '#111827';
+
+ return (
+
+
{label}
+ {payload.map(entry => (
+
+
+
+ {entry.name}
+
+ {entry.value}
+
+ ))}
+
+ );
+};
+
+const ReviewersRequirementChart = ({ duration, darkMode = false }) => {
+ const [data, setData] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const yAxisWidth = useYAxisWidth();
+
+ useEffect(() => {
+ const fetchAPIData = async () => {
+ setIsLoading(true);
+ setErrorMessage('');
+
+ try {
+ const token = window.localStorage.getItem('token');
+ const response = await axios.get(ENDPOINTS.GITHUB_REVIEW_SUMMARY(duration), {
+ headers: {
+ Authorization: token,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ setData(Array.isArray(response.data) ? response.data : []);
+ } catch (err) {
+ setData([]);
+ setErrorMessage('Unable to load GitHub review data for the selected duration.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchAPIData();
+ }, [duration]);
+
+ const sortedData = data
+ .map(normalizeReviewCounts)
+ .filter(item => item.reviewer)
+ .sort((a, b) => b.total - a.total);
+
+ if (isLoading) {
+ return Loading review data...
;
+ }
+
+ if (errorMessage) {
+ return {errorMessage}
;
+ }
+
+ if (!sortedData.length) {
+ return No review data found for this duration.
;
+ }
+
+ const axisColor = darkMode ? '#e2e8f0' : '#374151';
+ const gridColor = darkMode ? '#4a5568' : '#e5e7eb';
+ const labelColor = darkMode ? '#f8fafc' : '#111827';
+ const rowHeight = 34;
+ const chartHeight = Math.max(sortedData.length * rowHeight + 80, 360);
+
+ return (
+
+
+
+
+
+ (value?.length > 22 ? `${value.slice(0, 20)}…` : value)}
+ />
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ReviewersRequirementChart;
diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx
index ba7a177971..33c285c4f0 100644
--- a/src/components/Header/Header.jsx
+++ b/src/components/Header/Header.jsx
@@ -516,7 +516,7 @@ export function Header(props) {
)}
-
+
{TIMELOG}
@@ -839,6 +839,14 @@ export function Header(props) {
>
PR Analytics
+
+ Reviewers by Requirement
+
+
+
+
+
{/* /* for support team*/}
diff --git a/src/utils/URL.js b/src/utils/URL.js
index 73c277c374..bf771ffb02 100644
--- a/src/utils/URL.js
+++ b/src/utils/URL.js
@@ -688,6 +688,16 @@ export const ENDPOINTS = {
//pull requests analysis
PR_REVIEWS_INSIGHTS: `${APIEndpoint}/analytics/pr-review-insights`,
+ GITHUB_REVIEW_SUMMARY: (duration, sort = 'desc', team) => {
+ const params = new URLSearchParams({
+ duration,
+ sort,
+ });
+ if (team) {
+ params.set('team', team);
+ }
+ return `${APIEndpoint}/analytics/review-summary?${params.toString()}`;
+ },
PR_GRADING_CONFIG: `${APIEndpoint}/pr-grading-config`,
WEEKLY_GRADING: `${APIEndpoint}/weekly-grading`,
WEEKLY_GRADING_SAVE: `${APIEndpoint}/weekly-grading/save`,