diff --git a/src/components/PRGradingScreen/PRGradingScreen.jsx b/src/components/PRGradingScreen/PRGradingScreen.jsx index 8a369b4860..d7d88da8f6 100644 --- a/src/components/PRGradingScreen/PRGradingScreen.jsx +++ b/src/components/PRGradingScreen/PRGradingScreen.jsx @@ -1,8 +1,10 @@ +import axios from 'axios'; import { useState } from 'react'; import { Button, Card, Col, Container, Row } from 'react-bootstrap'; import { useSelector } from 'react-redux'; import { v4 as uuidv4 } from 'uuid'; import styles from './PRGradingScreen.module.css'; +import PromotionConfirmationBox from './PromotionConfirmationBox'; const PRGradingScreen = ({ teamData, reviewers }) => { const darkMode = useSelector(state => state.theme.darkMode); @@ -13,6 +15,10 @@ const PRGradingScreen = ({ teamData, reviewers }) => { const [inputError, setInputError] = useState(''); const [showGradingModal, setShowGradingModal] = useState(null); const [isFinalized, setIsFinalized] = useState(false); + const [promotionCandidate, setPromotionCandidate] = useState(null); + const [confirmedPromotions, setConfirmedPromotions] = useState([]); + const [selectedForPromotion, setSelectedForPromotion] = useState([]); + const [showBatchConfirm, setShowBatchConfirm] = useState(false); if (!teamData || !reviewers) { return
Error: Missing required props
; @@ -130,6 +136,81 @@ const PRGradingScreen = ({ teamData, reviewers }) => { const handleFinalize = () => { setIsFinalized(true); }; + /* ---------------- PROMOTION ---------------- */ + + const handlePromoteClick = async reviewer => { + try { + const response = await axios.get( + `${process.env.REACT_APP_APIENDPOINT}/promotion-details/${reviewer.id}`, + ); + setPromotionCandidate({ + reviewerId: reviewer.id, + reviewerName: response.data.reviewerName || reviewer.reviewer, + teamCode: response.data.teamCode || teamData.teamName, + teamReviewerName: response.data.teamReviewerName || reviewer.reviewer, + weeklyPRs: + response.data.weeklyPRs && response.data.weeklyPRs.length > 0 + ? response.data.weeklyPRs + : [{ week: teamData.dateRange.start, count: reviewer.gradedPrs.length }], + }); + } catch (error) { + // fallback to local data if API fails + setPromotionCandidate({ + reviewerId: reviewer.id, + reviewerName: reviewer.reviewer, + teamCode: teamData.teamName, + teamReviewerName: reviewer.reviewer, + weeklyPRs: [{ week: teamData.dateRange.start, count: reviewer.gradedPrs.length }], + }); + } + }; + + const handleConfirmPromotion = async (reviewerName, reviewerId) => { + try { + if (reviewerId) { + await axios.post(`${process.env.REACT_APP_APIENDPOINT}/promote-members`, { + memberIds: [reviewerId], + }); + } + } catch (error) { + // silently continue even if API fails + } + setConfirmedPromotions(prev => [...prev, reviewerName]); + setPromotionCandidate(null); + }; + + const handleCancelPromotion = () => { + setPromotionCandidate(null); + }; + /* ---------------- BATCH PROMOTION ---------------- */ + + const handleCheckboxChange = reviewerId => { + setSelectedForPromotion(prev => + prev.includes(reviewerId) ? prev.filter(id => id !== reviewerId) : [...prev, reviewerId], + ); + }; + + const handleBatchConfirm = async () => { + try { + if (selectedForPromotion.length > 0) { + await axios.post(`${process.env.REACT_APP_APIENDPOINT}/promote-members`, { + memberIds: selectedForPromotion, + }); + } + } catch (error) { + // silently continue + } + const selectedNames = reviewerData + .filter(r => selectedForPromotion.includes(r.id)) + .map(r => r.reviewer); + setConfirmedPromotions(prev => [...prev, ...selectedNames]); + setSelectedForPromotion([]); + setShowBatchConfirm(false); + }; + + const handleBatchCancel = () => { + setShowBatchConfirm(false); + }; /* ---------------- RENDER ---------------- */ @@ -195,7 +276,56 @@ const PRGradingScreen = ({ teamData, reviewers }) => { {reviewerData.map(reviewer => ( - {reviewer.reviewer} + +
+ {reviewer.reviewer} + {!confirmedPromotions.includes(reviewer.reviewer) && ( + handleCheckboxChange(reviewer.id)} + style={{ + marginTop: '4px', + cursor: 'pointer', + width: '16px', + height: '16px', + }} + /> + )} + {!confirmedPromotions.includes(reviewer.reviewer) ? ( + + ) : ( + + ✅ Promoted + + )} +
+ { )} + {selectedForPromotion.length > 0 && ( +
+ + {selectedForPromotion.length} selected + + + +
+ )} + {showBatchConfirm && ( +
+
+
+

+ 🏆 Confirm Batch Promotion +

+ +
+
+

+ You are about to promote {selectedForPromotion.length} reviewer(s): +

+
    + {reviewerData + .filter(r => selectedForPromotion.includes(r.id)) + .map(r => ( +
  • + {r.reviewer} +
  • + ))} +
+

+ Are you sure you want to promote all selected reviewers? +

+
+
+ + +
+
+
+ )} + {promotionCandidate && ( + + )} ); }; diff --git a/src/components/PRGradingScreen/PromotionConfirmationBox.jsx b/src/components/PRGradingScreen/PromotionConfirmationBox.jsx new file mode 100644 index 0000000000..2244539b6c --- /dev/null +++ b/src/components/PRGradingScreen/PromotionConfirmationBox.jsx @@ -0,0 +1,292 @@ +import PropTypes from 'prop-types'; +import styles from './PRGradingScreen.module.css'; + +function PromotionConfirmationBox({ reviewer, onConfirm, onCancel, darkMode }) { + const { reviewerName, teamCode, teamReviewerName, weeklyPRs } = reviewer; + + const totalPRs = weeklyPRs.reduce((sum, w) => sum + w.count, 0); + const avgPRs = weeklyPRs.length > 0 ? (totalPRs / weeklyPRs.length).toFixed(1) : 0; + const isConsistent = weeklyPRs.every(w => w.count >= 8); + + return ( +
+
+ {/* Header */} +
+

🏆 Confirm Promotion

+ +
+ + {/* Body */} +
+ {/* Reviewer Info */} +
+

+ Reviewer: {reviewerName} +

+

+ Team: {teamCode} +

+

+ Team Reviewer Name: {teamReviewerName} +

+
+ + {/* Weekly PR Stats */} +
+ Weekly PR Stats +
+ + + + + + + + + {weeklyPRs.map(w => ( + + + + + ))} + +
+ Week + + PR Count +
+ {w.week} + + {w.count} +
+ + {/* Summary Stats */} +
+
+
+ {totalPRs} +
+
+ Total PRs +
+
+
+
+ {avgPRs} +
+
+ Avg PRs/Week +
+
+
+
+ {isConsistent ? '✅' : '⚠️'} +
+
+ {isConsistent ? 'Consistent' : 'Inconsistent'} +
+
+
+ + {/* Confirmation question */} +

+ Are you sure you want to confirm promotion for{' '} + {reviewerName}? +

+
+ + {/* Footer */} +
+ + +
+
+
+ ); +} + +PromotionConfirmationBox.propTypes = { + reviewer: PropTypes.shape({ + reviewerId: PropTypes.string, + reviewerName: PropTypes.string.isRequired, + teamCode: PropTypes.string, + teamReviewerName: PropTypes.string, + weeklyPRs: PropTypes.arrayOf( + PropTypes.shape({ + week: PropTypes.string, + count: PropTypes.number, + }), + ), + }).isRequired, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + darkMode: PropTypes.bool, +}; + +PromotionConfirmationBox.defaultProps = { + darkMode: false, +}; + +export default PromotionConfirmationBox;