Skip to the content.

Gradebook

Gradebook table

Gradebook table

the folloowing is the response structure,

let respose = {
  allAssessmentDetails: [{
    assessmentTest: [{
      assessmentTestId: "",
      assessmentTestName: "",
      gradeColor: "",
      grade: "",
      marksObtained: "",
      percentage: "",
      testStatus: "",
      testComment: "",
    }],
    studentId: "",
    studentName: "",
  }],
  assessmentDetails: [{
    id: "",
    assessmentTitle: "",
    totalPoints: "",
    assessmentDate: "",
    markingSystem: "",
    convertTo: "",
    isIncludedInFinal: "",
    assessmentAverage: 0,
  }],
  defaultGradeScale: {
    gradeId: "",
    gradeName: "",
    gradeScaleConditions: [{
      id: "",
      gradePoints: "",
      gradeLetter: "",
      gradeLowerRange: "",
      gradeHigherRange: "",
      gradeColor: "",
      gradeOrder: 0,
    }]
  },
  allStudents: [{
    studentAverage: 0,
    studentName: "",
    studentId: "",
  }],
}

For ease of use and to make the data to be processable in ui and to be in correct format of data as frontend needed, we manuplate the data. Structure of the teacher store is given below,

const initialState = {
  list: [], // all classroom list
  isListLoaded: false, // whether we fetched the all classroom list or not

  pendingReq: {}, // pending students list

  // from `getClassDetails` data splited and stored seperately
  studentData: {},
  gradeScaleData: {},
  assessmentData: {},
  studentAssessmentData: {},
  loadedSections: {}
}

All objects are in the same type, like each data is stored by sectionId as key and their data is stored as value for that. While storing data as mentioned, loadedSections is set to true for that sectionId.

Data manuplation

  1. get data from api
  const {
    allStudents, // student list
    assessmentDetails, // assessment list
    allAssessmentDetails, // student marks in assessments
    defaultGradeScale, // grade scale details
  } = responseData
  1. sort assessmentDetails by date
  assessmentDetails.sort((a, b) => new Date(a.assessmentDate).getTime() < new Date(b.assessmentDate).getTime() ? 1 : -1)
  1. While initial develpoment on backend, student data is splited as roster and normal student and thier assessment details are stored with diffrent names. So we have to combine and change the name of fields, and add some data to help to render ui of the gradebook table (will be explained seperately).
  const studentAssessmentData = allAssessmentDetails.map(assessment => ({
    id: assessment.id || assessment.studentId, // student id can be just id or studentId
    studentName: assessment.studentName,
    assessmentTestForRoster: assessmentDetails.reduce((prev, current) => {
      const assessmentTest = assessment.assessmentTestForRoster || assessment.assessmentTest
      const details = assessmentTest.find(test => test.assessmentTestId === current.id)
      if (details) {
        prev.push({
          ...current,
          ...details
        })
      } else {
        prev.push({
          ...current,
          assessmentTestId: current.id,
          marksObtained: "-1",
          percentage: "-1",
          grade: "-1",
        })
      }

      return prev
    }, [])
  }))

As said before, assessment details name is saved differently, so we assign it as assessmentTestForRoster. then we are going to create a array for the student marks by assessment using assessmentDetails reduce method. If assessment id present in assessmentTest then teacher assigned mark to that test, if not we will add dummy data like above.

  1. After that we loop through student data once again. Since some students may not hav any marks assigned previously. like above same structure followed.
  allStudents.forEach(stu => {
    const isPresent = studentAssessmentData.some(stAs => stAs.id === stu.studentId || stAs.id === stu.id)

    if (!isPresent) {
      const newData = {
        id: stu.studentId || stu.id,
        studentName: stu.studentName,
        assessmentTestForRoster: assessmentDetails.map(assessment => ({
          ...assessment,
          assessmentTestId: assessment.id,
          marksObtained: "-1",
          percentage: "-1",
          grade: "-1",
        })),
      }

      studentAssessmentData.push(newData)
    }
  })
  1. Finally, we will update the gradescale data.
  const gradeScaleData = {
    gradeId: defaultGradeScale.gradeId,
    gradeName: titleDecider(defaultGradeScale.gradeName),
    gradeScaleConditions: defaultGradeScale.gradeScaleConditions
      .map(gr => ({
        ...gr,
        gradeHigherRange: Number(gr.gradeHigherRange),
        gradeLowerRange: Number(gr.gradeLowerRange),
        gradePoints: Number(gr.gradePoints),
      }))
      .sort((a, b) => a.gradeOrder > b.gradeOrder ? 1 : -1)
  }

For some gradeNames, we are using other names like point/percentage is called Default (Points/Percentage). So for handling that, we are using titleDecider function. gradeScaleConditions are not in correct order from backend, so we are sortig that data. And Finally we save all data to store.

Gradebook ui logics

The gradebook tab elated code all put together under comp/Teacher/Gradebook. comp/Teacher/Gradebook/Table is the heart of the gradebook.

  1. comp/Teacher/Gradebook We get studentAssessmentList, assessmentList, currentGradeScaleCondition and other data from the store. And store the other conditions as state like, displayAsPoint, filterByDate, sortBy, checked, etc.

Then we create gradeToMidPoints, gradeLettersOnly from currentGradeScaleCondition.

Then we calculate, update and filter data ([assessmentData, studentAssessmentData, avgScore]) from the following useMemo,

  const [assessmentData, studentAssessmentData, avgScore] = useMemo(() => {
    let newstudentAssessmentData = studentAssessmentList.map(p => ({
      ...p,
      avg: 0
    }))
    let newAssessmentData = assessmentList.map(p => p)

    if (checked.length > 0) {
      newAssessmentData = newAssessmentData.filter(li => checked.includes(li.id))
      newstudentAssessmentData = newstudentAssessmentData.map(li => ({
        ...li,
        assessmentTestForRoster: li.assessmentTestForRoster.filter(l => checked.includes(l.assessmentTestId))
      }))
    }

    if (filterByDate === "This week" || filterByDate === "Last week") {
      const toDate = filterByDate === "This week" ? new Date() : sub(new Date(), { days: 7 })
      newAssessmentData = newAssessmentData.filter(li => isSameWeek(new Date(li.assessmentDate), toDate))
      newstudentAssessmentData = newstudentAssessmentData.map(li => ({
        ...li,
        assessmentTestForRoster: li.assessmentTestForRoster.filter(l => isSameWeek(new Date(l.assessmentDate), toDate))
      }))
    }

    if (filterByDate === "This month" || filterByDate === "Last month") {
      const toDate = filterByDate === "This month" ? new Date() : sub(new Date(), { months: 1 })
      newAssessmentData = newAssessmentData.filter(li => isSameMonth(new Date(li.assessmentDate), toDate))
      newstudentAssessmentData = newstudentAssessmentData.map(li => ({
        ...li,
        assessmentTestForRoster: li.assessmentTestForRoster.filter(l => isSameMonth(new Date(l.assessmentDate), toDate))
      }))
    }

    if (filterByDate === "Last three months") {
      const today = new Date()
      const start = startOfMonth(sub(today, { months: 3 }))
      const end = endOfMonth(sub(today, { months: 1 }))

      newAssessmentData = newAssessmentData.filter(li => isWithinInterval(new Date(li.assessmentDate), { start, end }))
      newstudentAssessmentData = newstudentAssessmentData.map(li => ({
        ...li,
        assessmentTestForRoster: li.assessmentTestForRoster.filter(l => isWithinInterval(new Date(l.assessmentDate), { start, end }))
      }))
    }

    let assessMentAvg = findAvgPercentForAssessments(newAssessmentData, studentAssessmentList, gradeToMidPoints, currentGradeScaleCondition)

    newstudentAssessmentData.forEach(f => {
      if (f.assessmentTestForRoster.length > 0) {
        f.avg = findAvgPercentForStudent(f.assessmentTestForRoster)
      }
    })

    if (sortBy === "asc") {
      newstudentAssessmentData = newstudentAssessmentData.sort((a, b) => a.avg < b.avg ? 1 : -1)
    }

    if (sortBy === "desc") {
      newstudentAssessmentData = newstudentAssessmentData.sort((a, b) => a.avg > b.avg ? 1 : -1)
    }

    return [newAssessmentData, newstudentAssessmentData, assessMentAvg]
  }, [assessmentList, studentAssessmentList, checked, sortBy, filterByDate, gradeToMidPoints, currentGradeScaleCondition])

We are calculating average score for assessment as well as student in the useMemo using findAvgPercentForAssessments and findAvgPercentForStudent function respectively.

findAvgPercentForStudent

Simple function which accepts the assessment list as parameter and return the average value for the list.

findAvgPercentForAssessments

Function which return array of object to represent the average row of the gradebook table. We need to show different data as respective to Display as option.

parameters are assessments, studentAssessmentList, gradeToMidPoints, currentGradeScaleCondition

We are going to return new array based on assessments (using reduce method).

Notable points,

  1. comp/Teacher/Gradebook/Table We get data from props which is used to render table ui.
  assessmentData -> first row, assessment details row
  avgScore -> second row, average row
  studentAssessmentData -> other rows, student name and thier marks according to assessment order

Each array is used with different components. assessmentData is mapped with <HeadTr />, avgScore is mapped with <Mark />, studentAssessmentData is mapped with <Profile /> and its assessmentTestForRoster property is mapped with MarkInput />. MarkInput /> is breaked into several individual components.

<HeadTr />, <Mark />, <Profile /> are just ui components.

MarkInput

Gradebook cell

Props are self explanatory as the name suggest. i and j are the props which represent the placement in the table cell.

  const [final, setFinal] = useState(markingSystem === "0" ? Number(mark) : mark)

We will render different input boxes based on the requirement.

  // input value of the input box
  const [val, setVal] = useState(
    stuTestStatus === "1"
      ? "ab"
      : mark === "-1"
        ? ""
        : markingSystem === "0"
          ? convertTo > 0
            ? Number(Number(Number(mark) * (total / convertTo)).toFixed())
            : Number(mark)
          : mark
  )

Other functions explanations,

  // number input updater
  const onChangeByPoint = () => {}

  // grade letter input updater
  const onChangeByGrade = () => {}

  // check the input is valid to be enter as mark
  const canProceed = () => {}

  // onBlur -> fires on on blur of every input type. will make backend call only when the value changes.
  const onBlur = () => {}

  // onEnter -> fires when user pressed enter key
  const onEnter = () => {}

  // fires when onclicking the cell
  const onReEnter = () => {}

  // set the cell to be in focus
  const stayFocus = () => {}

  // on submiting, flag icon input (updating test status)
  const onFlagOptionClk = () => {}

  // on making comment for the cell
  const makePost = () => {}

PointInputBox , ConvertToPointBox, GradeInputBox are all same in logical wise which are used to update the input box and if the value is not fit to our condition then will fire error popup. In throughout our application react-aria is used GradeInputBox for select like input box. PointInputBox , ConvertToPointBox, CommentBox, TestStatusBox are using @floating-ui/react-dom-interactions library for placing the error popups. Infact most of the places we use @floating-ui/react-dom-interactions library. It is advised to learn this library before start working on.