Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | 2x 2x 39x 19x 20x 20x 2x 54x 54x 54x 54x 54x 54x 25x 25x 25x 25x 23x 23x 21x 20x 20x 20x 20x 21x 20x 20x 20x 2x 2x 25x 54x 25x 54x 54x 77x 54x | import { useState, useEffect } from "react";
import axios from "axios";
import { enqueueSnackbar } from "notistack";
import { Box } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { MRT_GlobalFilterTextField } from "material-react-table";
import PinnedTable from "@/utils/components/tables/pinned-table/PinnedTable";
import { getHeroSoftToolbarSx, getHeroSoftSearchFieldSx } from "@/utils/components/tables/heroTableStyles";
import { API_ENDPOINTS } from "@/api/endpoints";
import { useParams } from "react-router-dom";
// EI-1195: render a gradebook cell identically to the student assignment list.
// Numbers use the same formatting as the backend `format_grade` (integers show
// no decimals), and the ungraded states use the same labels the student sees, so
// the teacher gradebook and the student view can never appear to disagree.
export const STATUS_CELL_LABELS = {
missed: "Missed",
incomplete: "No submission",
pending: "Pending Review",
rejected: "Rejected",
};
export const formatGradeCell = (grade, status) => {
if (grade === null || grade === undefined) {
return STATUS_CELL_LABELS[status] ?? "No submission";
}
const num = Number(grade);
return Number.isInteger(num) ? String(num) : num.toFixed(2);
};
const Gradebook = () => {
const theme = useTheme();
const { classCode } = useParams();
const [columns, setColumns] = useState([]);
const [rows, setRows] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const fetchGradebookData = async () => {
const incomplete = "No submission";
setIsLoading(true);
try {
const response = await axios.get(API_ENDPOINTS.teacher.class.gradebook(classCode));
const { students } = response.data;
if (!Array.isArray(students) || students.length === 0) return;
// 1. Build test columns from first student
const testColumns = students[0].test_grades.map((test) => ({
accessorKey: test.assignment_id,
header: test.title || "Untitled Test",
}));
// 2. Build full column list
const dynamicColumns = [
{ accessorKey: "name.firstName", header: "First Name" },
{ accessorKey: "name.lastName", header: "Last Name" },
{ accessorKey: "name.middleName", header: "Middle Name" },
...testColumns,
{ accessorKey: "average", header: "Average" },
];
// 3. Build rows
const formattedRows = students.map((student) => {
const grades = {};
student.test_grades.forEach((test) => {
grades[test.assignment_id] = formatGradeCell(test.grade, test.status);
});
return {
name: {
firstName: student.first_name,
lastName: student.last_name,
middleName: student.middle_name,
},
...grades,
average:
student.average === null || student.average === undefined
? incomplete
: Number(student.average).toFixed(2),
};
});
setColumns(dynamicColumns);
setRows(formattedRows);
} catch (error) {
console.error("Error:", error.response?.data || error.message);
enqueueSnackbar("Failed to load gradebook data", { variant: "error" });
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchGradebookData();
}, []);
const pinnedColumn = { left: ["name.lastName"], right: ["average"] };
const topToolbar = ({ table }) => (
<Box sx={{ ...getHeroSoftToolbarSx(theme), alignItems: "center" }}>
<MRT_GlobalFilterTextField
table={table}
sx={getHeroSoftSearchFieldSx(theme)}
placeholder="Search students"
/>
</Box>
);
return (
<Box sx={{ width: "100%", minHeight: 420, display: "flex", flexDirection: "column" }}>
<Box sx={{ flex: 1, minHeight: 360, display: "flex", flexDirection: "column" }}>
<PinnedTable
tableId="teacher-gradebook"
variant="soft"
columns={columns}
data={rows}
topToolbar={topToolbar}
pinnedColumn={pinnedColumn}
isLoading={isLoading}
enableStickyHeader
muiTableContainerProps={{ sx: { minHeight: 260 } }}
/>
</Box>
</Box>
);
};
export default Gradebook;
|