diff --git a/package-lock.json b/package-lock.json index 3ef4561e..ec5213eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", + "chart.js": "^4.1.1", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,6 +3045,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5766,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -16603,6 +16616,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/pages/Authentication/Login.tsx b/src/pages/Authentication/Login.tsx index 2051b297..1db1c171 100644 --- a/src/pages/Authentication/Login.tsx +++ b/src/pages/Authentication/Login.tsx @@ -30,7 +30,7 @@ const Login: React.FC = () => { const onSubmit = (values: ILoginFormValues, submitProps: FormikHelpers) => { axios - .post("http://localhost:3002/login", values) + .post("http://152.7.177.55:3002/login", values) .then((response) => { const payload = setAuthToken(response.data.token); diff --git a/src/pages/ViewTeamGrades/Filters.tsx b/src/pages/ViewTeamGrades/Filters.tsx index 8e3d16c7..744cd8f1 100644 --- a/src/pages/ViewTeamGrades/Filters.tsx +++ b/src/pages/ViewTeamGrades/Filters.tsx @@ -1,17 +1,18 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import Dropdown from "react-bootstrap/Dropdown"; -import { useEffect } from "react"; type FiltersProps = { toggleShowReviews: () => void; toggleAuthorFeedback: () => void; selectRound: (v: number) => void; + totalRounds: number; // Number of rounds to display }; const Filters: React.FC = ({ toggleShowReviews, toggleAuthorFeedback, selectRound, + totalRounds, }) => { const [showSecondDropdown, setShowSecondDropdown] = useState(true); const [firstDropdownSelection, setFirstDropdownSelection] = useState("Reviews"); // Default text for the first dropdown button @@ -60,10 +61,10 @@ const Filters: React.FC = ({ setSecondDropdownSelection((prev) => { if (eventKey === "All Rounds") { selectRound(-1); - } else if (eventKey === "Round 1") { - selectRound(1); - } else if (eventKey === "Round 2") { - selectRound(2); + } else { + // Convert "Round 1" to round index 0, "Round 2" to 1, and so on + const roundIndex = parseInt(eventKey.split(" ")[1]) - 1; + selectRound(roundIndex); } return eventKey; }); // Update the second button text with the selected option @@ -109,15 +110,21 @@ const Filters: React.FC = ({ {secondDropdownSelection} + {/* Option for all rounds */} All rounds - - Round 1 - - - Round 2 - + + {/* Dynamically generate round options */} + {Array.from({ length: totalRounds }).map((_, index) => ( + + Round {index + 1} + + ))} diff --git a/src/pages/ViewTeamGrades/ReviewTable.tsx b/src/pages/ViewTeamGrades/ReviewTable.tsx index 5aa18db7..fb4d4c8f 100644 --- a/src/pages/ViewTeamGrades/ReviewTable.tsx +++ b/src/pages/ViewTeamGrades/ReviewTable.tsx @@ -1,30 +1,107 @@ import React, { useEffect, useState } from "react"; import ReviewTableRow from "./ReviewTableRow"; import RoundSelector from "./RoundSelector"; -import dummyDataRounds from "./Data/heatMapData.json"; -import dummyData from "./Data/dummyData.json"; -import { calculateAverages, getColorClass } from "./utils"; +import { calculateAverages } from "./utils"; import "./grades.scss"; import { Link } from "react-router-dom"; import Statistics from "./Statistics"; import Filters from "./Filters"; -import ShowReviews from "./ShowReviews"; //importing show reviews component -import dummyauthorfeedback from "./Data/authorFeedback.json"; // Importing dummy data for author feedback +import ShowReviews from "./ShowReviews"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store/store"; const ReviewTable: React.FC = () => { + const auth = useSelector( + (state: RootState) => state.authentication, + (prev, next) => prev.isAuthenticated === next.isAuthenticated + ); const [currentRound, setCurrentRound] = useState(-1); const [sortOrderRow, setSortOrderRow] = useState<"asc" | "desc" | "none">("none"); const [showToggleQuestion, setShowToggleQuestion] = useState(false); - const [open, setOpen] = useState(false); - const [teamMembers, setTeamMembers] = useState([]); - const [showReviews, setShowReviews] = useState(false); - const [ShowAuthorFeedback, setShowAuthorFeedback] = useState(false); const [roundSelected, setRoundSelected] = useState(-1); + const [data, setData] = useState(null); + const [isActionAllowed, setIsActionAllowed] = useState(false); useEffect(() => { - setTeamMembers(dummyData.members); + const fetchActionAllowed = async () => { + const token = localStorage.getItem("token"); + var show_page = false; + if (!token) { + + return; + } + + try { + console.log(auth.user) + if( auth.user.role=='Super Administrator'){ + show_page = true + }else{ + const response = await fetch("http://152.7.177.55:3002/api/v1/grades/action_allowed?requested_action=view_my_scores", { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const result = await response.json(); + show_page = result['allowed'] + } + + if (show_page === true) { + setIsActionAllowed(true); + + const response1 = await fetch("http://152.7.177.55:3002/api/v1/grades/view_grading_report?id=1", { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const resp1 = await response1.json(); + + const response2 = await fetch("http://152.7.177.55:3002/api/v1/grades/view_my_scores?id=1", { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const resp2 = await response2.json(); + + const response3 = await fetch("http://152.7.177.55:3002/api/v1/grades/view_team?id=1", { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const resp3 = await response3.json(); + + setData({ + team: resp3.team, + team_information: resp3.users, + questions: resp2.questions?.review || [], + summary: resp2.summary || {}, + avg_scores_by_round: resp2.avg_scores_by_round || {}, + avg_scores_by_criterion: resp2.avg_scores_by_criterion || {}, + review_score_count: resp1.review_score_count || 0, + participant: resp3.participant || {}, + assignment: resp2.assignment || {}, + roundsOfReviews: resp2.assignment?.rounds_of_reviews || 1, + }); + } + } catch (error) { + console.error("Error fetching action_allowed API:", error); + } + }; + + fetchActionAllowed(); }, []); + if (!isActionAllowed) { + return
Action not allowed. Please contact support.
; + } + + if (!data) { + return
Loading...
; + } + const toggleSortOrderRow = () => { setSortOrderRow((prevSortOrder) => { if (prevSortOrder === "asc") return "desc"; @@ -33,19 +110,6 @@ const ReviewTable: React.FC = () => { }); }; - const toggleShowReviews = () => { - setShowReviews((prev) => !prev); - }; - - const selectRound = (r: number) => { - setRoundSelected((prev) => r); - }; - - // Function to toggle the visibility of ShowAuthorFeedback component - const toggleAuthorFeedback = () => { - setShowAuthorFeedback((prev) => !prev); - }; - const handleRoundChange = (roundIndex: number) => { setCurrentRound(roundIndex); }; @@ -54,16 +118,46 @@ const ReviewTable: React.FC = () => { setShowToggleQuestion(!showToggleQuestion); }; - const renderTable = (roundData: any, roundIndex: number) => { + const generateRoundData = () => { + const reviewerComments = data.summary?.["1"] || {}; + const avgScoresByCriterion = data.avg_scores_by_criterion?.["1"] || {}; + + return data.questions.map((q: any) => { + const comments = reviewerComments[q.txt] || []; + const avgScore = avgScoresByCriterion[q.txt] ?? 0; + + return { + questionNumber: q.id, + questionText: q.txt, + reviews: comments.map((comment: string) => ({ + score: avgScore / 100, + comment: comment, + })), + RowAvg: avgScore / 100, + maxScore: 5, + }; + }); + }; + + const getReviewScoreCount = () => { + if (data?.summary && data.summary["1"]) { + return Object.keys(data.summary["1"]).length; + } + return 0; + }; + + const renderTable = (roundData: any[], roundIndex: number) => { const { averagePeerReviewScore, columnAverages, sortedData } = calculateAverages( roundData, sortOrderRow ); return ( +
+

- Review (Round: {roundIndex + 1} of {dummyDataRounds.length}) + Review (Round: {roundIndex + 1})

@@ -77,9 +171,7 @@ const ReviewTable: React.FC = () => { )} {Array.from({ length: roundData[0].reviews.length }, (_, i) => ( - + ))} - + {showToggleQuestion && } {columnAverages.map((avg, index) => (
{`Review ${ - i + 1 - }`}{`Review ${i + 1}`} Average @@ -94,9 +186,7 @@ const ReviewTable: React.FC = () => { ))}
- Avg - Avg @@ -118,52 +208,40 @@ const ReviewTable: React.FC = () => { return (
-

Summary Report: Program 2

-
Team: {dummyData.team}
+

Summary Report: {data.assignment?.name || "Assignment"}

+
Team: {data.team.name}
- Team members:{" "} - {teamMembers.map((member, index) => ( - - {member} - {index !== teamMembers.length - 1 && ", "} - - ))} - - + Team members:{" "} + { + // Declare an empty array to hold the team members + (() => { + const teamMembers = []; + for (let index = 0; index < data.team_information.length; index++) { + const member = data.team_information[index]; + teamMembers.push( + + {member.name} ({member.email}) {/* Display the user's name and email */} + {index !== data.team_information.length - 1 && ", "} {/* Add a comma if it's not the last member */} + + ); + } + return teamMembers; + })() + } + +
- +
+
- +
{
- {/* Conditionally render tables based on currentRound */} + {/* Render tables */} {currentRound === -1 - ? dummyDataRounds.map((roundData, index) => renderTable(roundData, index)) // Render a table for each round if "All Rounds" is selected - : renderTable(dummyDataRounds[currentRound], currentRound)} + ? [0].map((_, index) => renderTable(generateRoundData(), index)) + : renderTable(generateRoundData(), currentRound)}
{}} + toggleAuthorFeedback={() => {}} + selectRound={setRoundSelected} + totalRounds = {data.roundsOfReviews} />
- {showReviews && ( -
-

Reviews

- -
- )} - {ShowAuthorFeedback && ( -
-

Author Feedback

- -
- )} +
-

+

Grade and comment for submission

- Grade: {dummyData.grade} -
- Comment: {dummyData.comment} -
- Late Penalty: {dummyData.late_penalty} -
-

+

+ Grade: {data.participant?.grade ?? "N/A"} +
+ Comment: No Comment Available +
+ Late Penalty: No Late Penalty +

+
Back
diff --git a/src/pages/ViewTeamGrades/RoundSelector.tsx b/src/pages/ViewTeamGrades/RoundSelector.tsx index 103362d1..43b1bbe5 100644 --- a/src/pages/ViewTeamGrades/RoundSelector.tsx +++ b/src/pages/ViewTeamGrades/RoundSelector.tsx @@ -1,18 +1,18 @@ -import React, { useState, useEffect } from "react"; -import dummyDataRounds from "./Data/heatMapData.json"; -import teamData from "./Data/dummyData.json"; +import React from "react"; interface RoundSelectorProps { currentRound: number; handleRoundChange: (roundIndex: number) => void; + totalRounds: number; // The number of rounds to display } // RoundSelector component to display buttons for selecting rounds -const RoundSelector: React.FC = ({ currentRound, handleRoundChange }) => { +const RoundSelector: React.FC = ({ currentRound, handleRoundChange, totalRounds }) => { return (
- {/* Mapping over dummyDataRounds to render round buttons */} + + {/* Button for All Rounds */} - {dummyDataRounds.map((round, index) => ( + {/* Buttons for specific rounds */} + {Array.from({ length: totalRounds }).map((_, index) => ( ))} - {/* Displaying team members */} +
); diff --git a/src/pages/ViewTeamGrades/ShowReviews.tsx b/src/pages/ViewTeamGrades/ShowReviews.tsx index 570a412e..1461c87c 100644 --- a/src/pages/ViewTeamGrades/ShowReviews.tsx +++ b/src/pages/ViewTeamGrades/ShowReviews.tsx @@ -1,92 +1,97 @@ -import React from "react"; -import { getColorClass } from "./utils"; -import { RootState } from "../../store/store"; -import { useDispatch, useSelector } from "react-redux"; +import React, { useState, useEffect } from "react"; -//props for the ShowReviews -interface ReviewComment { - score: number; - comment?: string; - name: string; -} - -interface Review { - questionNumber: string; - questionText: string; - reviews: ReviewComment[]; - RowAvg: number; - maxScore: number; +interface Question { + id: number; + txt: string; + question_type: string; } interface ShowReviewsProps { - data: Review[][]; + questions: Question[]; + summary: any; roundSelected: number; + avg_scores_by_criterion: any; // new } -//function for ShowReviews -const ShowReviews: React.FC = ({ data, roundSelected }) => { - console.log("round selected: ", roundSelected); - const rounds = data.length; +const getBubbleColorHex = (score: number) => { + if (score >= 4.5) return "c5"; // Dark green + if (score >= 3.5) return "c4"; // Green + if (score >= 2.5) return "c3"; // Yellow + if (score >= 1.5) return "c2"; // Orange + return "c1"; // Red +}; - const auth = useSelector( - (state: RootState) => state.authentication, - (prev, next) => prev.isAuthenticated === next.isAuthenticated - ); - // Render each review for every question in each round - const renderReviews = () => { - const reviewElements: JSX.Element[] = []; - for (let r = 0; r < rounds; r++) { - if (roundSelected === 1) { - if (r == 1) { - continue; - } - } - if (roundSelected === 2) { - if (r == 0) { - continue; - } - } - const num_of_questions = data[r].length; +const ShowReviews: React.FC = ({ + questions, + summary, + roundSelected, + avg_scores_by_criterion, +}) => { + const [reviewScores, setReviewScores] = useState({}); - // Assuming 'reviews' array exists inside the first 'question' of the first 'round'. - const num_of_reviews = data[r][0].reviews.length; - reviewElements.push(
Round {r + 1}
); - for (let i = 0; i < num_of_reviews; i++) { - if (auth.user.role !== "Student") { - reviewElements.push(
Review {i + 1}
); - } else { - reviewElements.push(
Review {i + 1}
); - } - for (let j = 0; j < num_of_questions; j++) { - reviewElements.push( -
-
- {j + 1}. {data[r][j].questionText} -
-
- - {data[r][j].reviews[i].score} - - {data[r][j].reviews[i].comment && ( -
{data[r][j].reviews[i].comment}
- )} -
-
- ); - } - } + useEffect(() => { + // Map question text -> scaled score + const scoresMap: any = {}; + if (avg_scores_by_criterion?.["1"]) { + Object.keys(avg_scores_by_criterion["1"]).forEach((questionText) => { + const rawScore = avg_scores_by_criterion["1"][questionText]; + scoresMap[questionText] = rawScore / 100; // Proper scaling to 5 + }); } + setReviewScores(scoresMap); + }, [avg_scores_by_criterion]); - return reviewElements; - }; + if (!questions.length) return
No questions available.
; - return
{rounds > 0 ? renderReviews() :
No reviews available
}
; + const participantSummary = summary["1"] || {}; + + return ( +
+

Reviews

+ + {questions.map((question, questionIndex) => ( +
+
+ {questionIndex + 1}. {question.txt} +
+ +
+ {(participantSummary[question.txt] || []).map((comment: any, i: number) => ( +
+ {/* Bubble for score */} +
+ {reviewScores[question.txt] ?? "N/A"} +
+
{comment}
+
+ ))} +
+
+ ))} +
+ ); }; -export default ShowReviews; +export default ShowReviews; \ No newline at end of file diff --git a/src/pages/ViewTeamGrades/Statistics.tsx b/src/pages/ViewTeamGrades/Statistics.tsx index e26d175a..4d1d9e12 100644 --- a/src/pages/ViewTeamGrades/Statistics.tsx +++ b/src/pages/ViewTeamGrades/Statistics.tsx @@ -1,121 +1,67 @@ -// Statistics.tsx -import React, { useState, useEffect } from "react"; -import { calculateAverages } from "./utils"; -import "./grades.scss"; -import dummyDataRounds from "./Data/heatMapData.json"; // Importing dummy data for rounds -import dummyauthorfeedback from "./Data/authorFeedback.json"; // Importing dummy data for author feedback -import teammateData from "./Data/teammateData.json"; +import React from "react"; -//props for statistics component -interface StatisticsProps {} - -//statistics component -const Statistics: React.FC = () => { - const [sortedData, setSortedData] = useState([]); - useEffect(() => { - const { averagePeerReviewScore, columnAverages, sortedData } = calculateAverages( - dummyDataRounds[0], - "asc" - ); - const rowAvgArray = sortedData.map((item) => item.RowAvg); - console.log(rowAvgArray); - setSortedData(sortedData.map((item) => item.RowAvg)); - }, []); - - const [statisticsVisible, setstatisticsVisible] = useState(false); - const toggleStatisticsVisibility = () => { - setstatisticsVisible(!statisticsVisible); - }; - const [showReviews, setShowReviews] = useState(false); - const [ShowAuthorFeedback, setShowAuthorFeedback] = useState(false); - - const [roundSelected, setRoundSelected] = useState(-1); - - const selectRound = (r: number) => { - setRoundSelected((prev) => r); - }; - - // Function to toggle the visibility of ShowReviews component - const toggleShowReviews = () => { - setShowReviews((prev) => !prev); - }; - - // Function to toggle the visibility of ShowAuthorFeedback component - const toggleAuthorFeedback = () => { - setShowAuthorFeedback((prev) => !prev); - }; +interface StatisticsProps { + avg_scores_by_round: Record; + avg_scores_by_criterion: Record>; + review_score_count: number; + summary: Record>; +} +const Statistics: React.FC = ({ + avg_scores_by_round, + avg_scores_by_criterion, + review_score_count, + summary, +}) => { const headerCellStyle: React.CSSProperties = { - padding: "10px", - textAlign: "center", + padding: "8px", + backgroundColor: "#f2f2f2", + fontWeight: "bold", + border: "1px solid black", }; - //calculation for total reviews recieved - let totalReviewsForQuestion1: number = 0; - dummyDataRounds.forEach((round) => { - round.forEach((question) => { - if (question.questionNumber === "1") { - totalReviewsForQuestion1 += question.reviews.length; - } - }); - }); - //calculation for total feedback recieved - let totalfeedbackForQuestion1: number = 0; - dummyauthorfeedback.forEach((round) => { - round.forEach((question) => { - if (question.questionNumber === "1") { - totalfeedbackForQuestion1 += question.reviews.length; - } - }); - }); - const subHeaderCellStyle: React.CSSProperties = { - padding: "10px", - textAlign: "center", + padding: "8px", + backgroundColor: "#ffffff", + fontWeight: "normal", + border: "1px solid black", }; + // Calculate total number of reviews for the first question + // const totalReviewsForQuestion1 = Object.values(summary).reduce((sum, reviewee) => { + // return sum + (reviewee["What is the main purpose of this feature?"]?.length || 0); + // }, 0); + return ( -
+
+ {/*

Review Statistics

*/} + + {/*

Total Reviews for Question 1: {totalReviewsForQuestion1}

*/} + {/*

Review Score Count: {review_score_count}

*/}
Round Summary
- - - - - + + + + + - {dummyDataRounds.map((roundData, index) => { - // Calculate averages for each category using data from utils or manually. - const submittedWorkAvg = calculateAverages(roundData, "asc").averagePeerReviewScore; - const authorFeedbackAvg = - dummyauthorfeedback[index]?.reduce((acc, item) => { - const questionScoreSum = item.reviews.reduce( - (sum, review) => sum + review.score, - 0 - ); - return acc + questionScoreSum / item.reviews.length; - }, 0) / dummyauthorfeedback[index].length; - - const teammateReviewAvg = - teammateData[index]?.reviews.reduce((acc, review) => acc + review.score, 0) / - teammateData[index]?.reviews.length; + {Object.entries(avg_scores_by_round).map(([round, submittedAvg]) => { + const submittedWorkAvg = submittedAvg; // always a number - const finalScore = ( - (Number(submittedWorkAvg) + Number(authorFeedbackAvg) + Number(teammateReviewAvg)) / - 3 - ).toFixed(2); // Average of all three categories + const finalScore = submittedWorkAvg ?? "N/A"; // <-- this fixes the error return ( - - - - - - + + + + + + ); })} @@ -125,4 +71,4 @@ const Statistics: React.FC = () => { ); }; -export default Statistics; +export default Statistics; \ No newline at end of file
RoundSubmitted Work (Avg)Author Feedback (Avg)Teammate Review (Avg)Final ScoreRoundSubmitted Work (Avg)Author Feedback (Avg)Teammate Review (Avg)Final Score
Round {index + 1}{Number(submittedWorkAvg).toFixed(2)}{authorFeedbackAvg?.toFixed(2) || "N/A"}{teammateReviewAvg?.toFixed(2) || "N/A"}{finalScore}
{`Round ${round}`}{submittedWorkAvg}N/AN/A{finalScore}