diff --git a/src/components/PRGradingDashboard/GradingTable.jsx b/src/components/PRGradingDashboard/GradingTable.jsx
index 156b769640a..beb5d1bd443 100644
--- a/src/components/PRGradingDashboard/GradingTable.jsx
+++ b/src/components/PRGradingDashboard/GradingTable.jsx
@@ -10,7 +10,10 @@ function GradingTable({
onAddPRClick,
openAddModal,
onAddGradedPR,
+ onRemoveReviewer,
+ isCurrentWeek,
darkMode,
+ canEdit,
}) {
return (
//
@@ -30,7 +33,10 @@ function GradingTable({
grading={grading}
onUpdatePRsReviewed={onUpdatePRsReviewed}
onAddPRClick={onAddPRClick}
+ onRemoveReviewer={onRemoveReviewer}
+ isCurrentWeek={isCurrentWeek}
darkMode={darkMode}
+ canEdit={canEdit}
/>
{openAddModal === grading.reviewer && (
@@ -62,12 +68,17 @@ GradingTable.propTypes = {
onAddPRClick: PropTypes.func.isRequired,
openAddModal: PropTypes.string,
onAddGradedPR: PropTypes.func.isRequired,
+ onRemoveReviewer: PropTypes.func.isRequired,
+ isCurrentWeek: PropTypes.bool,
darkMode: PropTypes.bool,
+ canEdit: PropTypes.bool,
};
GradingTable.defaultProps = {
openAddModal: null,
+ isCurrentWeek: true,
darkMode: false,
+ canEdit: false,
};
export default GradingTable;
diff --git a/src/components/PRGradingDashboard/PRGradingDashboard.jsx b/src/components/PRGradingDashboard/PRGradingDashboard.jsx
index 82e8a9afe38..6c075ebbcc1 100644
--- a/src/components/PRGradingDashboard/PRGradingDashboard.jsx
+++ b/src/components/PRGradingDashboard/PRGradingDashboard.jsx
@@ -1,18 +1,57 @@
import axios from 'axios';
import { useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import { toast } from 'react-toastify';
import AddReviewerModal from './AddReviewerModal';
import ConfirmationModal from './ConfirmationModal';
import GradingTable from './GradingTable';
+import RemoveReviewerModal from './RemoveReviewerModal';
import { MOCK_WEEK_METADATA, MOCK_WEEK_OPTIONS } from './mockWeeklyAuditData';
import styles from './PRGradingDashboard.module.css';
import { SelectionProvider } from './SelectionContext';
import SummaryList from './SummaryList';
+import hasPermission from '~/utils/permissions';
-const TEAM_CODE = 'TeamA';
const TEAM_NAME = 'Team Alpha';
+const formatLocalDate = date => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+};
+
+const getWeekStart = () => {
+ const now = new Date();
+ const day = now.getDay();
+ const monday = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate() - day + (day === 0 ? -6 : 1),
+ );
+ return formatLocalDate(monday);
+};
+
+const WEEK_OPTION_OFFSETS = {
+ 'Current Week': 0,
+ '1 Week Ago': 1,
+ '2 Weeks Ago': 2,
+ '3 Weeks Ago': 3,
+ '4 Weeks Ago': 4,
+};
+
+const getWeekStartByOffset = weeksAgo => {
+ const now = new Date();
+ const day = now.getDay();
+ const mondayOffset = day === 0 ? -6 : 1 - day;
+ const monday = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate() + mondayOffset - weeksAgo * 7,
+ );
+ return formatLocalDate(monday);
+};
+
// Mock data for fallback
const mockData = [
{
@@ -36,7 +75,10 @@ const mockData = [
];
function PRGradingDashboard() {
+ const dispatch = useDispatch();
const darkMode = useSelector(state => state.theme.darkMode);
+ const authUser = useSelector(state => state.auth.user);
+ const canEdit = dispatch(hasPermission('accessPRTeamDashboard'));
const [gradings, setGradings] = useState([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -44,18 +86,22 @@ function PRGradingDashboard() {
const [pendingPR, setPendingPR] = useState(null); // { reviewer, prNumbers, grade } or null
const [showConfirmation, setShowConfirmation] = useState(false);
const [showAddReviewerModal, setShowAddReviewerModal] = useState(false);
+ const [pendingRemoveReviewer, setPendingRemoveReviewer] = useState(null); // reviewer name or null
const [selectedMockWeek, setSelectedMockWeek] = useState('Current Week');
+ const [selectedTeam, setSelectedTeam] = useState('TeamA');
- // Fetch data on mount
+ // Fetch data on mount and whenever the selected week changes
useEffect(() => {
- fetchGradings();
- }, []);
+ void fetchGradings(selectedMockWeek);
+ }, [selectedMockWeek]);
- const fetchGradings = async () => {
+ const fetchGradings = async (weekLabel = 'Current Week') => {
+ const offset = WEEK_OPTION_OFFSETS[weekLabel] ?? 0;
+ const weekDate = getWeekStartByOffset(offset);
try {
setLoading(true);
const response = await axios.get(
- `${process.env.REACT_APP_APIENDPOINT}/weekly-grading?team=${TEAM_CODE}`,
+ `${process.env.REACT_APP_APIENDPOINT}/weekly-grading?team=${selectedTeam}&date=${weekDate}`,
);
if (response.data && Array.isArray(response.data)) {
// Mark all existing PRs as not new
@@ -153,6 +199,39 @@ function PRGradingDashboard() {
toast.success(`Reviewer "${newReviewer.reviewer}" added successfully.`);
};
+ // Request to remove a reviewer (shows confirmation modal)
+ const requestRemoveReviewer = reviewer => {
+ setPendingRemoveReviewer(reviewer);
+ };
+
+ // Actually remove the reviewer after confirmation
+ const confirmRemoveReviewer = async () => {
+ if (!pendingRemoveReviewer) return;
+ const offset = WEEK_OPTION_OFFSETS[selectedMockWeek] ?? 0;
+ const weekDate = getWeekStartByOffset(offset);
+ try {
+ await axios.delete(
+ `${
+ process.env.REACT_APP_APIENDPOINT
+ }/weekly-grading?team=${selectedTeam}&date=${weekDate}&reviewer=${encodeURIComponent(
+ pendingRemoveReviewer,
+ )}`,
+ { data: { requestor: { requestorId: authUser.userid } } },
+ );
+ setGradings(prev => prev.filter(g => g.reviewer !== pendingRemoveReviewer));
+ toast.success(`Reviewer "${pendingRemoveReviewer}" removed successfully.`);
+ } catch (error) {
+ toast.error('Failed to remove reviewer. Please try again.');
+ } finally {
+ setPendingRemoveReviewer(null);
+ }
+ };
+
+ // Cancel reviewer removal
+ const cancelRemoveReviewer = () => {
+ setPendingRemoveReviewer(null);
+ };
+
// Remove a graded PR (only new ones)
const removeGradedPR = (reviewer, prIndex) => {
// Check if PR can be removed before updating state
@@ -191,12 +270,11 @@ function PRGradingDashboard() {
const handleSave = async () => {
try {
setSaving(true);
- const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
-
// Remove isNew flag before sending to API
+ const offset = WEEK_OPTION_OFFSETS[selectedMockWeek] ?? 0;
const payload = {
- teamCode: TEAM_CODE,
- date: currentDate,
+ teamCode: selectedTeam,
+ date: getWeekStartByOffset(offset),
gradings: gradings.map(g => ({
reviewer: g.reviewer,
prsReviewed: g.prsReviewed,
@@ -225,6 +303,8 @@ function PRGradingDashboard() {
}
};
+ const isCurrentWeek = (WEEK_OPTION_OFFSETS[selectedMockWeek] ?? 0) === 0;
+
const currentDate = new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
@@ -279,22 +359,27 @@ function PRGradingDashboard() {
{/* Main Table */}
-
-
-
+ {isCurrentWeek && (
+
+
+
+ )}
@@ -311,16 +396,18 @@ function PRGradingDashboard() {
{/* Footer */}
-
-
-
+ {isCurrentWeek && (
+
+
+
+ )}
{/* Confirmation Modal */}
@@ -334,6 +421,15 @@ function PRGradingDashboard() {
/>
)}
+ {/* Remove Reviewer Modal */}
+ {pendingRemoveReviewer && (
+
+ )}
+
{/* Add Reviewer Modal */}
{showAddReviewerModal && (
setShowAddReviewerModal(false)} />
diff --git a/src/components/PRGradingDashboard/RemoveReviewerModal.jsx b/src/components/PRGradingDashboard/RemoveReviewerModal.jsx
new file mode 100644
index 00000000000..ce33d180797
--- /dev/null
+++ b/src/components/PRGradingDashboard/RemoveReviewerModal.jsx
@@ -0,0 +1,85 @@
+import { useEffect } from 'react';
+import { Trash2, X } from 'lucide-react';
+import styles from './ConfirmationModal.module.css';
+
+function RemoveReviewerModal({ reviewer, onConfirm, onCancel }) {
+ useEffect(() => {
+ const handleEscape = e => {
+ if (e.key === 'Escape') onCancel();
+ };
+ document.addEventListener('keydown', handleEscape);
+ return () => document.removeEventListener('keydown', handleEscape);
+ }, [onCancel]);
+
+ const handleOverlayClick = e => {
+ if (e.target === e.currentTarget) onCancel();
+ };
+
+ const handleOverlayKeyDown = e => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onCancel();
+ }
+ };
+
+ return (
+
+
+
+
+ Remove Reviewer
+
+
+
+
+
+
+
+
+ Remove reviewer {reviewer} from this week?
+
+
+ Their past PR review history will remain visible in analytics.
+
+
+
+
+
+
+
+
+ );
+}
+
+export default RemoveReviewerModal;
diff --git a/src/components/PRGradingDashboard/ReviewerRow.jsx b/src/components/PRGradingDashboard/ReviewerRow.jsx
index 6a7c197ec7d..55cfa0a001f 100644
--- a/src/components/PRGradingDashboard/ReviewerRow.jsx
+++ b/src/components/PRGradingDashboard/ReviewerRow.jsx
@@ -1,9 +1,17 @@
-import { Plus } from 'lucide-react';
+import { Plus, Trash2 } from 'lucide-react';
import PropTypes from 'prop-types';
import styles from './ReviewerRow.module.css';
import { useRowSelection } from './SelectionContext';
-function ReviewerRow({ grading, onUpdatePRsReviewed, onAddPRClick, darkMode }) {
+function ReviewerRow({
+ grading,
+ onUpdatePRsReviewed,
+ onAddPRClick,
+ onRemoveReviewer,
+ isCurrentWeek,
+ darkMode,
+ canEdit,
+}) {
const { activeId, selectRow } = useRowSelection();
const handlePRsReviewedChange = e => {
@@ -49,14 +57,29 @@ function ReviewerRow({ grading, onUpdatePRsReviewed, onAddPRClick, darkMode }) {
{grading.prsNeeded}
-
+ {isCurrentWeek && canEdit ? (
+
+
+
+
+ ) : (
+ Read only
+ )}
|
);
@@ -69,11 +92,16 @@ ReviewerRow.propTypes = {
}).isRequired,
onUpdatePRsReviewed: PropTypes.func.isRequired,
onAddPRClick: PropTypes.func.isRequired,
+ onRemoveReviewer: PropTypes.func.isRequired,
+ isCurrentWeek: PropTypes.bool,
darkMode: PropTypes.bool,
+ canEdit: PropTypes.bool,
};
ReviewerRow.defaultProps = {
+ isCurrentWeek: true,
darkMode: false,
+ canEdit: false,
};
export default ReviewerRow;
diff --git a/src/components/PRGradingDashboard/ReviewerRow.module.css b/src/components/PRGradingDashboard/ReviewerRow.module.css
index 5bb515de925..83e1af3de21 100644
--- a/src/components/PRGradingDashboard/ReviewerRow.module.css
+++ b/src/components/PRGradingDashboard/ReviewerRow.module.css
@@ -73,6 +73,47 @@
margin-right: 0.5rem;
}
+.actionsCell {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.removeButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.5rem;
+ background-color: transparent;
+ color: #6b7280;
+ border: 1px solid #d1d5db;
+ border-radius: 0.375rem;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.removeButton:hover {
+ background-color: #fef2f2;
+ color: #dc2626;
+ border-color: #fca5a5;
+}
+
+.removeButton:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgb(220 38 38 / 50%);
+}
+
+.removeButtonIcon {
+ width: 1rem;
+ height: 1rem;
+}
+
+.readOnlyLabel {
+ font-size: 0.75rem;
+ color: #9ca3af;
+ font-style: italic;
+}
+
/* Dark mode support */
:global(.dark-mode) .row:hover {
background-color: #374151;
@@ -108,4 +149,15 @@
:global(.dark-mode) .addButton:focus {
box-shadow: 0 0 0 2px rgb(37 99 235 / 50%), 0 0 0 4px rgb(37 99 235 / 10%);
+}
+
+:global(.dark-mode) .removeButton {
+ color: #9ca3af;
+ border-color: #4b5563;
+}
+
+:global(.dark-mode) .removeButton:hover {
+ background-color: #450a0a;
+ color: #f87171;
+ border-color: #991b1b;
}
\ No newline at end of file
diff --git a/src/routes.jsx b/src/routes.jsx
index b0cad32696f..cc72c8bab02 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -1257,7 +1257,7 @@ export default (
path="/pr-grading-dashboard"
exact
component={PRGradingDashboard}
- fallback
+ routePermissions={RoutePermissions.prDashboard}
/>