All files / src/app/student-page/components/assignments RubricTable.jsx

100% Statements 6/6
100% Branches 13/13
100% Functions 3/3
100% Lines 6/6

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                                                                1x 9x 2x     7x                       13x                                                   37x                                                                                
/**
 * EI-3182 — RubricTable
 *
 * Presentational component that renders a structured assignment rubric.
 * Accepts the canonical rubric shape:
 *   { criteria: [ { name: string, levels: [ { label: string, points: number, description?: string } ] } ] }
 *
 * Renders nothing when rubric is absent or has no criteria rows.
 *
 * Developer: Allan Niñal
 */
 
import React from "react";
import {
    Box,
    Card,
    CardContent,
    Chip,
    Divider,
    Table,
    TableBody,
    TableCell,
    TableContainer,
    TableHead,
    TableRow,
    Typography,
} from "@mui/material";
import GradingIcon from "@mui/icons-material/Grading";
 
/**
 * @param {{ rubric: import("../types").Rubric | null | undefined }} props
 */
const RubricTable = ({ rubric }) => {
    if (!rubric || !Array.isArray(rubric.criteria) || rubric.criteria.length === 0) {
        return null;
    }
 
    return (
        <Card elevation={2} id="student-assignment-rubric-card" sx={{ mb: 3 }} data-testid="rubric-card">
            <CardContent>
                <Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
                    <GradingIcon sx={{ fontSize: 30, color: "primary.main", mr: 1.5 }} />
                    <Typography variant="h6" fontWeight={600}>
                        Rubric
                    </Typography>
                </Box>
                <Divider sx={{ mb: 2 }} />
 
                {rubric.criteria.map((criterion, criterionIndex) => (
                    <Box key={criterionIndex} sx={{ mb: criterionIndex < rubric.criteria.length - 1 ? 3 : 0 }}>
                        <Typography
                            variant="subtitle1"
                            fontWeight={600}
                            sx={{ mb: 1 }}
                            data-testid={`rubric-criterion-name-${criterionIndex}`}
                        >
                            {criterion.name}
                        </Typography>
 
                        <TableContainer>
                            <Table
                                size="small"
                                aria-label={`Rubric criteria: ${criterion.name}`}
                                data-testid={`rubric-criterion-table-${criterionIndex}`}
                            >
                                <TableHead>
                                    <TableRow>
                                        <TableCell sx={{ fontWeight: 600, width: 120 }}>Level</TableCell>
                                        <TableCell sx={{ fontWeight: 600, width: 80 }}>Points</TableCell>
                                        <TableCell sx={{ fontWeight: 600 }}>Description</TableCell>
                                    </TableRow>
                                </TableHead>
                                <TableBody>
                                    {Array.isArray(criterion.levels) &&
                                        criterion.levels.map((level, levelIndex) => (
                                            <TableRow
                                                key={levelIndex}
                                                data-testid={`rubric-level-row-${criterionIndex}-${levelIndex}`}
                                                sx={{
                                                    "&:last-child td, &:last-child th": { border: 0 },
                                                }}
                                            >
                                                <TableCell>
                                                    <Chip
                                                        label={level.label}
                                                        size="small"
                                                        color="primary"
                                                        variant="outlined"
                                                    />
                                                </TableCell>
                                                <TableCell>
                                                    <Typography variant="body2" fontWeight={500}>
                                                        {level.points}
                                                    </Typography>
                                                </TableCell>
                                                <TableCell>
                                                    <Typography variant="body2" color="text.secondary">
                                                        {level.description ?? ""}
                                                    </Typography>
                                                </TableCell>
                                            </TableRow>
                                        ))}
                                </TableBody>
                            </Table>
                        </TableContainer>
 
                        {criterionIndex < rubric.criteria.length - 1 && <Divider sx={{ mt: 2 }} />}
                    </Box>
                ))}
            </CardContent>
        </Card>
    );
};
 
export default RubricTable;