diff --git a/src/actions/prAnalytics/weeklyGradingActions.js b/src/actions/prAnalytics/weeklyGradingActions.js
new file mode 100644
index 0000000000..067a6fc0c5
--- /dev/null
+++ b/src/actions/prAnalytics/weeklyGradingActions.js
@@ -0,0 +1,32 @@
+import axios from 'axios';
+import { ENDPOINTS } from '../../utils/URL';
+
+export const getWeeklyGrading = (teamCode, date, token) => async dispatch => {
+ try {
+ const params = { team: teamCode };
+ if (date) params.date = date;
+
+ const response = await axios.get(ENDPOINTS.WEEKLY_GRADING, {
+ params,
+ headers: { Authorization: `${token}` },
+ });
+ return response.data;
+ } catch (error) {
+ dispatch({ type: 'SET_ERROR', error: error.message });
+ throw error;
+ }
+};
+
+export const saveWeeklyGrading = (teamCode, date, gradings, token) => async dispatch => {
+ try {
+ const response = await axios.post(
+ ENDPOINTS.WEEKLY_GRADING_SAVE,
+ { teamCode, date, gradings },
+ { headers: { Authorization: `${token}` } },
+ );
+ return response.data;
+ } catch (error) {
+ dispatch({ type: 'SET_ERROR', error: error.message });
+ throw error;
+ }
+};
\ No newline at end of file
diff --git a/src/components/PRGradingScreen/PRGradingScreen.jsx b/src/components/PRGradingScreen/PRGradingScreen.jsx
index 8a369b4860..11f05e0132 100644
--- a/src/components/PRGradingScreen/PRGradingScreen.jsx
+++ b/src/components/PRGradingScreen/PRGradingScreen.jsx
@@ -1,11 +1,18 @@
-import { useState } from 'react';
+import PropTypes from 'prop-types';
+import { useEffect, useState } from 'react';
import { Button, Card, Col, Container, Row } from 'react-bootstrap';
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import { v4 as uuidv4 } from 'uuid';
+import {
+ getWeeklyGrading,
+ saveWeeklyGrading,
+} from '../../actions/prAnalytics/weeklyGradingActions';
import styles from './PRGradingScreen.module.css';
const PRGradingScreen = ({ teamData, reviewers }) => {
const darkMode = useSelector(state => state.theme.darkMode);
+ const token = useSelector(state => state.auth.token);
+ const dispatch = useDispatch();
const [reviewerData, setReviewerData] = useState(reviewers || []);
const [activeInput, setActiveInput] = useState(null);
@@ -13,6 +20,39 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
const [inputError, setInputError] = useState('');
const [showGradingModal, setShowGradingModal] = useState(null);
const [isFinalized, setIsFinalized] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveMessage, setSaveMessage] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (!teamData?.teamCode) return;
+
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const data = await dispatch(
+ getWeeklyGrading(teamData.teamCode, teamData.dateRange?.start, token),
+ );
+ if (data && data.length > 0) {
+ const mapped = data.map(entry => ({
+ id: uuidv4(),
+ reviewer: entry.reviewer,
+ prsNeeded: entry.prsNeeded,
+ prsReviewed: entry.prsReviewed,
+ gradedPrs: (entry.gradedPrs || []).map(pr => ({ ...pr, id: uuidv4() })),
+ }));
+ setReviewerData(mapped);
+ }
+ } catch {
+ // fallback to prop data
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [teamData?.teamCode]);
if (!teamData || !reviewers) {
return
Error: Missing required props
;
@@ -23,20 +63,11 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
const validatePRNumber = value => {
const trimmed = value.trim();
const pattern = /^\d+(\s*\+\s*\d+)?$/;
-
- if (!trimmed) {
- return { isValid: false, error: 'PR number cannot be empty' };
- }
-
- if (!pattern.test(trimmed)) {
- return { isValid: false, error: 'Format: 1070 or 1070 + 1256' };
- }
-
+ if (!trimmed) return { isValid: false, error: 'PR number cannot be empty' };
+ if (!pattern.test(trimmed)) return { isValid: false, error: 'Format: 1070 or 1070 + 1256' };
return { isValid: true, error: '' };
};
- const isBackendFrontendPair = value => value.includes('+');
-
/* ---------------- ADD PR ---------------- */
const handleAddNewClick = reviewerId => {
@@ -48,7 +79,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
const handleInputSubmit = reviewerId => {
if (isFinalized) return;
-
const validation = validatePRNumber(inputValue);
if (!validation.isValid) {
setInputError(validation.error);
@@ -56,8 +86,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
}
const trimmed = inputValue.trim();
-
- // Duplicate check
const normalize = str => str.replace(/\s/g, '').toLowerCase();
const reviewer = reviewerData.find(r => r.id === reviewerId);
const isDuplicate = reviewer?.gradedPrs.some(
@@ -65,26 +93,16 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
);
if (isDuplicate) {
- setInputError(
- 'This PR already exists for this reviewer. Please enter a different PR number.',
- );
+ setInputError('This PR already exists for this reviewer.');
return;
}
- const newPREntry = {
- id: uuidv4(),
- prNumbers: trimmed,
- grade: 'Okay',
- };
+ const newPREntry = { id: uuidv4(), prNumbers: trimmed, grade: 'Okay' };
setReviewerData(prev =>
prev.map(r =>
r.id === reviewerId
- ? {
- ...r,
- gradedPrs: [...r.gradedPrs, newPREntry],
- prsReviewed: r.gradedPrs.length + 1,
- }
+ ? { ...r, gradedPrs: [...r.gradedPrs, newPREntry], prsReviewed: r.gradedPrs.length + 1 }
: r,
),
);
@@ -95,7 +113,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
};
const handleCancel = () => {
- if (isFinalized) return;
setActiveInput(null);
setInputValue('');
setInputError('');
@@ -110,7 +127,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
const handleGradeChange = (reviewerId, prId, newGrade) => {
if (isFinalized) return;
-
setReviewerData(prev =>
prev.map(r =>
r.id === reviewerId
@@ -123,16 +139,37 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
);
};
- const handleCloseGradingModal = () => {
- setShowGradingModal(null);
- };
+ const handleCloseGradingModal = () => setShowGradingModal(null);
- const handleFinalize = () => {
- setIsFinalized(true);
+ /* ---------------- SAVE / FINALIZE ---------------- */
+
+ const handleFinalize = async () => {
+ setIsSaving(true);
+ setSaveMessage('');
+ try {
+ const gradings = reviewerData.map(r => ({
+ reviewer: r.reviewer,
+ prsReviewed: r.gradedPrs.length,
+ prsNeeded: r.prsNeeded,
+ gradedPrs: r.gradedPrs.map(pr => ({ prNumbers: pr.prNumbers, grade: pr.grade })),
+ }));
+
+ await dispatch(
+ saveWeeklyGrading(teamData.teamCode, teamData.dateRange?.start, gradings, token),
+ );
+ setIsFinalized(true);
+ setSaveMessage('Grades saved successfully!');
+ } catch {
+ setSaveMessage('Error saving grades. Please try again.');
+ } finally {
+ setIsSaving(false);
+ }
};
/* ---------------- RENDER ---------------- */
+ if (isLoading) return Loading...
;
+
return (
{
{teamData.teamName} - {teamData.dateRange.start} to {teamData.dateRange.end}
-
-
+
+ {saveMessage && (
+ {saveMessage}
+ )}
+
+
@@ -191,98 +232,156 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
PR Numbers |
-
{reviewerData.map(reviewer => (
-
- | {reviewer.reviewer} |
-
-
-
- |
-
- {reviewer.prsNeeded} |
-
-
- {reviewer.gradedPrs.map(pr => (
- handlePRNumberClick(reviewer.id)}
- onKeyDown={e => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- handlePRNumberClick(reviewer.id);
- }
- }}
- >
- {pr.prNumbers}
-
- ))}
-
- {!isFinalized && activeInput !== reviewer.id && (
-
- )}
-
- {!isFinalized && activeInput === reviewer.id && (
-
- {
- setInputValue(e.target.value);
- setInputError('');
- }}
- className={`${styles['pr-grading-screen-pr-number-input']} ${
- inputError ? styles['pr-grading-screen-input-error'] : ''
+ <>
+
+ | {reviewer.reviewer} |
+
+
+ |
+ {reviewer.prsNeeded} |
+
+ {reviewer.gradedPrs.map(pr => (
+
+ onClick={() => handlePRNumberClick(reviewer.id)}
+ onKeyDown={e => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handlePRNumberClick(reviewer.id);
+ }
+ }}
+ >
+ {pr.prNumbers}
+
+ ))}
+ {!isFinalized && activeInput !== reviewer.id && (
-
- |
+
- {inputError && (
-
- {inputError}
-
- )}
-
- )}
- |
-
+ {/* Inline summary row per reviewer */}
+ {reviewer.gradedPrs.length > 0 && (
+
+ |
+
+ |
+
+ )}
+ >
))}
@@ -315,7 +414,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
×
-
{
Exceptional |
Okay |
Unsatisfactory |
-
Cannot find image |
+
No Correct Image |
-
{reviewerData
.find(r => r.id === showGradingModal)
@@ -374,9 +471,9 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
- handleGradeChange(showGradingModal, pr.id, 'Cannot find image')
+ handleGradeChange(showGradingModal, pr.id, 'No Correct Image')
}
/>
@@ -384,7 +481,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
))}
-
{
);
};
+PRGradingScreen.propTypes = {
+ teamData: PropTypes.shape({
+ teamCode: PropTypes.string,
+ teamName: PropTypes.string,
+ dateRange: PropTypes.shape({
+ start: PropTypes.string,
+ end: PropTypes.string,
+ }),
+ }).isRequired,
+ reviewers: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ reviewer: PropTypes.string,
+ prsNeeded: PropTypes.number,
+ prsReviewed: PropTypes.number,
+ gradedPrs: PropTypes.array,
+ }),
+ ).isRequired,
+};
+
export default PRGradingScreen;
diff --git a/src/components/PRGradingScreen/PRGradingScreen.module.css b/src/components/PRGradingScreen/PRGradingScreen.module.css
index 774b4c0227..1706cdfec1 100644
--- a/src/components/PRGradingScreen/PRGradingScreen.module.css
+++ b/src/components/PRGradingScreen/PRGradingScreen.module.css
@@ -894,4 +894,16 @@
background-color: #383d41;
color: #e2e3e5;
border-color: #d6d8db;
+}
+
+.pr-grading-screen-summary-row {
+ background-color: #f8f9fa;
+}
+
+.pr-grading-screen-summary-row.dark-mode {
+ background-color: #1e2a3a;
+}
+
+.pr-grading-screen-summary-cell {
+ padding: 0.5rem 1rem !important;
}
\ No newline at end of file
diff --git a/src/utils/URL.js b/src/utils/URL.js
index 27204eab5d..4f7f5491f7 100644
--- a/src/utils/URL.js
+++ b/src/utils/URL.js
@@ -573,6 +573,8 @@ export const ENDPOINTS = {
//pull requests analysis
PR_REVIEWS_INSIGHTS: `${APIEndpoint}/analytics/pr-review-insights`,
PR_GRADING_CONFIG: `${APIEndpoint}/pr-grading-config`,
+ WEEKLY_GRADING: `${APIEndpoint}/weekly-grading`,
+ WEEKLY_GRADING_SAVE: `${APIEndpoint}/weekly-grading/save`,
// Education Portal endpoints
STUDENT_PROFILE: `${APIEndpoint}/student/profile`,