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} +
+ )} +
+ )} + + {reviewer.gradedPrs.length > 0 && ( + + )} + + - {inputError && ( -
- {inputError} -
- )} - - )} - - + {/* Inline summary row per reviewer */} + {reviewer.gradedPrs.length > 0 && ( + + + + + + + + + + + + + + {reviewer.gradedPrs.map(pr => ( + + + {[ + 'Exceptional', + 'Okay', + 'Unsatisfactory', + 'No Correct Image', + ].map(grade => ( + + ))} + + ))} + +
PR NumberExceptionalOkayUnsatisfactoryNo Correct Image
{pr.prNumbers} + + handleGradeChange(reviewer.id, pr.id, grade) + } + /> +
+ + + )} + ))} @@ -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`,