All files / src/app/teacher-page/page/gradebook Gradebook.jsx

100% Statements 39/39
100% Branches 20/20
100% Functions 8/8
100% Lines 37/37

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;