diff --git a/src/components/PRGradingScreen/PRGradingScreen.jsx b/src/components/PRGradingScreen/PRGradingScreen.jsx index 4c96cf1d2f..8605c5c2c6 100644 --- a/src/components/PRGradingScreen/PRGradingScreen.jsx +++ b/src/components/PRGradingScreen/PRGradingScreen.jsx @@ -1,9 +1,11 @@ +import axios from 'axios'; import PropTypes from 'prop-types'; import { useMemo, 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); @@ -14,10 +16,19 @@ const PRGradingScreen = ({ teamData, reviewers }) => { const [inputError, setInputError] = useState(''); const [showGradingModal, setShowGradingModal] = useState(null); const [isFinalized, setIsFinalized] = useState(false); - - // Search state const [searchTerm, setSearchTerm] = useState(''); const [roleFilter, setRoleFilter] = useState(''); + const [promotionCandidate, setPromotionCandidate] = useState(null); + const [confirmedPromotions, setConfirmedPromotions] = useState(() => { + try { + const stored = localStorage.getItem(`promotedReviewers_${teamData?.teamName}`); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } + }); + const [selectedForPromotion, setSelectedForPromotion] = useState([]); + const [showBatchConfirm, setShowBatchConfirm] = useState(false); /* ---------------- SEARCH FILTER ---------------- */ @@ -111,8 +122,89 @@ const PRGradingScreen = ({ teamData, reviewers }) => { }; const handleCloseGradingModal = () => setShowGradingModal(null); + 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) { + 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 + } + setConfirmedPromotions(prev => { + const updated = [...prev, reviewerName]; + localStorage.setItem(`promotedReviewers_${teamData?.teamName}`, JSON.stringify(updated)); + return updated; + }); + 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 => { + const updated = [...prev, ...selectedNames]; + localStorage.setItem(`promotedReviewers_${teamData?.teamName}`, JSON.stringify(updated)); + return updated; + }); + setSelectedForPromotion([]); + setShowBatchConfirm(false); + }; + + const handleBatchCancel = () => setShowBatchConfirm(false); + /* ---------------- RENDER ---------------- */ const dm = darkMode ? styles['dark-mode'] : ''; @@ -144,7 +236,7 @@ const PRGradingScreen = ({ teamData, reviewers }) => { - {/* ── Search Bar ── */} + {/* Search Bar */}
{ onChange={e => setSearchTerm(e.target.value)} className={`${styles['pr-grading-screen-search-input']} ${dm}`} /> - {availableRoles.length > 0 && ( )} - {(searchTerm || roleFilter) && ( + + ) : ( + + ✅ Promoted + + )} +
+ { setInputValue(e.target.value)} - className={styles['pr-grading-screen-pr-number-input']} + onChange={e => { + setInputValue(e.target.value); + setInputError(''); + }} + className={`${styles['pr-grading-screen-pr-number-input']} ${ + inputError ? styles['pr-grading-screen-input-error'] : '' + } ${dm}`} placeholder="1070 or 1070 + 1256" /> + {inputError && ( +
+ {inputError} +
+ )} )} @@ -289,7 +439,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => { × -
@@ -349,7 +498,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => { ))}
-
)} + + {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 index 2244539b6c..35f5da2ac3 100644 --- a/src/components/PRGradingScreen/PromotionConfirmationBox.jsx +++ b/src/components/PRGradingScreen/PromotionConfirmationBox.jsx @@ -229,7 +229,7 @@ function PromotionConfirmationBox({ reviewer, onConfirm, onCancel, darkMode }) { className={`${styles['pr-grading-screen-modal-footer']} ${ darkMode ? styles['dark-mode'] : '' }`} - style={{ gap: '12px' }} + style={{ gap: '12px', padding: '16px 24px', marginTop: '8px' }} >