Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/components/PRGradingDashboard/GradingTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ function GradingTable({
onAddPRClick,
openAddModal,
onAddGradedPR,
onRemoveReviewer,
isCurrentWeek,
darkMode,
canEdit,
}) {
return (
// <div className={styles.tableWrapper}>
Expand All @@ -30,7 +33,10 @@ function GradingTable({
grading={grading}
onUpdatePRsReviewed={onUpdatePRsReviewed}
onAddPRClick={onAddPRClick}
onRemoveReviewer={onRemoveReviewer}
isCurrentWeek={isCurrentWeek}
darkMode={darkMode}
canEdit={canEdit}
/>
{openAddModal === grading.reviewer && (
<tr className={styles.modalRow}>
Expand Down Expand Up @@ -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;
156 changes: 126 additions & 30 deletions src/components/PRGradingDashboard/PRGradingDashboard.jsx
Original file line number Diff line number Diff line change
@@ -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 = [
{
Expand All @@ -36,26 +75,33 @@
];

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);
const [openAddModal, setOpenAddModal] = useState(null); // reviewer name or null
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');

Check warning on line 91 in src/components/PRGradingDashboard/PRGradingDashboard.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of the unused 'setSelectedTeam' variable.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AaFEK-wL-6adpPWi-wYQ&open=AaFEK-wL-6adpPWi-wYQ&pullRequest=5596

Check warning on line 91 in src/components/PRGradingDashboard/PRGradingDashboard.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The value assigned to "setSelectedTeam" is never read. Use it or remove this assignment.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AaFEK-wL-6adpPWi-wYR&open=AaFEK-wL-6adpPWi-wYR&pullRequest=5596

// 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
Expand Down Expand Up @@ -153,6 +199,39 @@
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 {

Check warning on line 225 in src/components/PRGradingDashboard/PRGradingDashboard.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception, don't catch it at all, or explain in a comment why it is ignored.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AaDvdyn_NlXKlOMtXV30&open=AaDvdyn_NlXKlOMtXV30&pullRequest=5596
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
Expand Down Expand Up @@ -191,12 +270,11 @@
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,
Expand Down Expand Up @@ -225,6 +303,8 @@
}
};

const isCurrentWeek = (WEEK_OPTION_OFFSETS[selectedMockWeek] ?? 0) === 0;

const currentDate = new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
Expand Down Expand Up @@ -279,22 +359,27 @@
{/* Main Table */}
<SelectionProvider>
<div className={styles.section}>
<div className={styles.tableHeaderActions}>
<button
type="button"
onClick={() => setShowAddReviewerModal(true)}
className={styles.addReviewerButton}
>
+ Add Reviewer
</button>
</div>
{isCurrentWeek && (
<div className={styles.tableHeaderActions}>
<button
type="button"
onClick={() => setShowAddReviewerModal(true)}
className={styles.addReviewerButton}
>
+ Add Reviewer
</button>
</div>
)}
<GradingTable
gradings={gradings}
onUpdatePRsReviewed={updatePRsReviewed}
onAddPRClick={setOpenAddModal}
openAddModal={openAddModal}
onAddGradedPR={requestAddGradedPR}
onRemoveReviewer={requestRemoveReviewer}
isCurrentWeek={isCurrentWeek}
darkMode={darkMode}
canEdit={canEdit}
/>
</div>

Expand All @@ -311,16 +396,18 @@

{/* Footer */}

<div className={styles.footerContent}>
<button
onClick={handleSave}
disabled={saving}
type="button"
className={styles.saveButton}
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
{isCurrentWeek && (
<div className={styles.footerContent}>
<button
onClick={handleSave}
disabled={saving}
type="button"
className={styles.saveButton}
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
)}
</div>

{/* Confirmation Modal */}
Expand All @@ -334,6 +421,15 @@
/>
)}

{/* Remove Reviewer Modal */}
{pendingRemoveReviewer && (
<RemoveReviewerModal
reviewer={pendingRemoveReviewer}
onConfirm={confirmRemoveReviewer}
onCancel={cancelRemoveReviewer}
/>
)}

{/* Add Reviewer Modal */}
{showAddReviewerModal && (
<AddReviewerModal onAdd={addReviewer} onCancel={() => setShowAddReviewerModal(false)} />
Expand Down
85 changes: 85 additions & 0 deletions src/components/PRGradingDashboard/RemoveReviewerModal.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={styles.overlay}
onClick={handleOverlayClick}
onKeyDown={handleOverlayKeyDown}
role="button"
tabIndex={0}
aria-label="Close modal by clicking outside"
>

Check warning on line 33 in src/components/PRGradingDashboard/RemoveReviewerModal.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <input type="button">, <input type="image">, <input type="reset">, <input type="submit">, or <button> instead of the "button" role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AaDvdyv4NlXKlOMtXV31&open=AaDvdyv4NlXKlOMtXV31&pullRequest=5596
<div
className={styles.modal}
role="dialog"
aria-modal="true"
aria-labelledby="remove-reviewer-modal-title"
>

Check warning on line 39 in src/components/PRGradingDashboard/RemoveReviewerModal.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <dialog> instead of the "dialog" role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AaDvdyv4NlXKlOMtXV32&open=AaDvdyv4NlXKlOMtXV32&pullRequest=5596
<div className={styles.modalHeader}>
<h3 id="remove-reviewer-modal-title" className={styles.modalTitle}>
Remove Reviewer
</h3>
<button
type="button"
onClick={onCancel}
className={styles.closeButton}
aria-label="Close"
>
<X className={styles.closeIcon} />
</button>
</div>
<div className={styles.modalBody}>
<div className={styles.confirmationIcon}>
<Trash2 className={styles.icon} />
</div>
<p className={styles.confirmationText}>
Remove reviewer <strong>{reviewer}</strong> from this week?
</p>
<p className={styles.noteText}>
Their past PR review history will remain visible in analytics.
</p>
</div>
<div className={styles.modalFooter}>
<button
type="button"
onClick={onCancel}
className={`${styles.button} ${styles.buttonCancel}`}
>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
className={`${styles.button} ${styles.buttonConfirm}`}
>
Remove
</button>
</div>
</div>
</div>
);
}

export default RemoveReviewerModal;
Loading
Loading