All files / src/app/teacher-page/page/assignments/data-analytics/item-analysis AnalysisTable.jsx

96.87% Statements 31/32
81.25% Branches 13/16
80% Functions 4/5
96.77% Lines 30/31

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                                              2x                     2x                             2x 34x 34x 34x 34x   34x 34x   34x         12x 12x 2x 2x   10x 10x 10x     3x 3x     7x 7x       5x 5x 5x                   10x       34x 12x     34x   34x                               34x                
import { useEffect, useMemo, useState } from "react";
import { MaterialReactTable, useMaterialReactTable } from "material-react-table";
import { API_ENDPOINTS } from "@/api/endpoints";
import { useLocation, useParams } from "react-router-dom";
import axios from "axios";
import React from "react";
import SimpleLoading from "@/utils/components/SimpleLoading";
import { useUpgradeDialog } from "@/utils/components/feature-access/UpgradeDialogProvider";
import useFeatureFlag from "@/utils/hooks/useFeatureFlag";
import {
    getFeatureMeta,
    isFeatureNotEnabled403,
    parseFeatureKeyFrom403,
} from "@/utils/features/featureAccess";
import { sanitizeRichText } from "@/utils/helpers/sanitizeHtml";
import { transformData } from "./itemAnalysisTransform";
 
// Answer-choice cell values may contain pre-rendered math-formula markup (see
// itemAnalysisTransform.extractContentHtml — MathLive's static output, e.g.
// `<span class="mfe-formula" data-latex="…">…</span>`) instead of plain text.
// MaterialReactTable's default cell rendering would print that markup as raw
// text, so these columns render through this instead, sanitized before
// insertion since it's staff-authored question content.
const RichAnalysisCell = ({ cell }) => (
    <span
        dangerouslySetInnerHTML={{ __html: sanitizeRichText(String(cell.getValue() ?? "")) }}
        style={{ display: "inline" }}
    />
);
 
// Item Analysis table columns. Exported so the layout can be unit-tested without
// rendering MaterialReactTable. NOTE (EI-3390): there must be NO phantom leading
// column (a stray `accessorKey:"test"` with an empty header used to corrupt the
// table) — the first column is "No." and every column maps to a real row field.
export const ITEM_ANALYSIS_COLUMNS = [
    { accessorKey: "question_number", header: "No.", size: 10 },
    { accessorKey: "question_type", header: "Question Type", size: 10 },
    { accessorKey: "category", header: "Category", size: 10 },
    { accessorKey: "student_expectations", header: "S.E.", size: 10 },
    { accessorKey: "answer_choiceA", header: "A", size: 10, Cell: RichAnalysisCell },
    { accessorKey: "answer_choiceB", header: "B", size: 10, Cell: RichAnalysisCell },
    { accessorKey: "answer_choiceC", header: "C", size: 10, Cell: RichAnalysisCell },
    { accessorKey: "answer_choiceD", header: "D", size: 10, Cell: RichAnalysisCell },
    { accessorKey: "correct_answer", header: "Correct Answer", size: 10 },
    { accessorKey: "points", header: "Points", size: 10 },
    { accessorKey: "percent_correct", header: "% Correct", size: 10 },
    { accessorKey: "percent_incorrect", header: "% Incorrect", size: 10 },
];
 
const MyTable = () => {
    const location = useLocation();
    const { classCode } = useParams();
    const { openUpgradeDialog } = useUpgradeDialog();
    const { features, getFeature } = useFeatureFlag("teacher");
 
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(false);
 
    const fetchItemAnalysis = async () => {
        // The page receives the assignment id via in-app navigation state. On a
        // hard reload / deep-link there is no `location.state`, so reading
        // `location.state.assignmentId` directly threw → blank table. Guard it and
        // render the empty table (headers, no rows) instead of crashing. (EI-3390)
        const assignmentId = location.state?.assignmentId;
        if (!assignmentId) {
            setData([]);
            return;
        }
        try {
            setLoading(true);
            const response = await axios.get(
                API_ENDPOINTS.teacher.assignment.analytics.itemAnalysis(classCode, assignmentId)
            );
            Eif (response.status == 200) {
                setData(transformData(response.data));
            }
        } catch (error) {
            console.error("Error:", error.response?.data || error.message);
            if (isFeatureNotEnabled403(error)) {
                // `isFeatureNotEnabled403` above is true only when `parseFeatureKeyFrom403` already
                // produced a key, so the old `?? fallback` here could never be reached.
                // Removed rather than left to imply the fallback matters. (Allan Ninal)
                const featureKey = parseFeatureKeyFrom403(error);
                const featureMeta = getFeature(featureKey) || getFeatureMeta(features, featureKey);
                openUpgradeDialog({
                    title: "Item analysis is locked",
                    featureKey,
                    featureLabel: "Item Analysis",
                    featureMeta,
                    minPlan: featureMeta?.min_plan ?? "intermediate",
                });
            }
        } finally {
            // Always clear loading — a non-200 2xx used to leave the spinner stuck. (EI-188 r2)
            setLoading(false);
        }
    };
 
    useEffect(() => {
        fetchItemAnalysis();
    }, []);
 
    const columns = useMemo(() => ITEM_ANALYSIS_COLUMNS, []);
 
    const table = useMaterialReactTable({
        columns,
        data,
        muiTableHeadCellProps: {
            sx: {
                fontWeight: "normal",
                fontSize: "12px",
            },
        },
        muiTableBodyCellProps: {
            sx: {
                fontSize: "12px",
            },
        },
    });
 
    return (
        <React.Fragment>
            {loading ? <SimpleLoading showLoading={loading} /> : <MaterialReactTable table={table} />}
        </React.Fragment>
    );
};
 
export default MyTable;