+
+
+
School Year Decisions
+
Whole Year • {schoolYear || 'Current school year'}
+
+
+
+ Back to Below 60
+
+
+ Grading
+
+
+
+
+
+
+
+
+
+
+
Students
+
{metrics.total}
+
+
+
+
+
+
+
Pending
+
{metrics.pending}
+
+
+
+
+
+
+
Decided
+
{metrics.decided}
+
+
+
+
+
+
+
Year Score Under 50
+
{metrics.urgent}
+
+
+
+
+
+ {success ?
{success}
: null}
+ {error ?
{error}
: null}
+ {loading ?
Loading decisions…
: null}
+
+ {!loading && rows.length === 0 ? (
+
No students below 60 for the whole year in {schoolYear}.
+ ) : null}
+
+ {!loading && rows.length > 0 ? (
+ <>
+
+
+
+
+ | Student Name |
+ Age |
+ Class-Section |
+ Year Score |
+ Comments / Rationale & Decision |
+ Below-60 Decision |
+
+
+
+ {rows.map((row) => {
+ const studentId = Number(row.student_id ?? 0)
+ const yearScore = numericScore(row.year_score)
+ const rowClass = yearScore !== null ? (yearScore < 50 ? 'table-danger' : 'table-warning') : ''
+ const studentName = `${fieldText(row.firstname)} ${fieldText(row.lastname)}`.trim() || 'N/A'
+ const decision = fieldText(row.decision)
+ const notes = fieldText(row.decision_notes)
+ const badgeClass = decisionBadgeClass(decision)
+ const isSaving = savingStudentId === studentId
+ const emailHref = `${GRADING_BELOW_SIXTY_DECISION_EMAIL_EDITOR_PATH}?student_id=${studentId}&school_year=${encodeURIComponent(schoolYear)}`
+
+ return (
+
+ | {studentName} |
+ {fieldText(row.age) || '—'} |
+ {fieldText(row.class_section_name) || '—'} |
+
+ {displayScore(row.year_score)}
+
+ |
+
+
+ |
+
+ {decision !== '' && badgeClass ? (
+ <>
+
+ {decision}
+
+
+ Send Email
+
+ >
+ ) : (
+ Pending
+ )}
+ |
+
+ )
+ })}
+
+
+
+
+
+ {DECISION_OPTIONS.filter((option) => option.value !== '').map((option) => {
+ const badgeClass = decisionBadgeClass(option.value)
+ if (!badgeClass) return null
+ return (
+
+ {option.label}
+
+ )
+ })}
+
+ >
+ ) : null}
+
+ {detailsOpen ? (
+
setDetailsOpen(false)}
+ >
+
event.stopPropagation()}>
+
+
+
Score Details{modalStudentName ? ` — ${modalStudentName}` : ''}
+
+
+ {modalError ?
{modalError}
: null}
+ {modalLoading ?
Loading details…
: null}
+
+ {!modalLoading && !modalError && modalSemesters.length === 0 ? (
+
No detailed score data found.
+ ) : null}
+
+ {!modalLoading &&
+ !modalError &&
+ modalSemesters.map((semester) => (
+
+
+ {fieldText(semester.semester) || 'Semester'}
+ {fieldText(semester.class_section_name) ? ` — ${fieldText(semester.class_section_name)}` : ''}
+
+
+
+
+
+ | Item |
+ Score |
+
+
+
+ {[
+ ['Homework Avg', semester.homework_avg],
+ ['Project Avg', semester.project_avg],
+ ['Participation', semester.participation_score],
+ ['Test Avg', semester.test_avg],
+ ['PTAP Score', semester.ptap_score],
+ ['Attendance', semester.attendance_score],
+ ['Midterm Score', semester.midterm_exam_score],
+ ['Final Exam', semester.final_exam_score],
+ ['Semester Score', semester.semester_score],
+ ]
+ .filter(([, value]) => value !== null && value !== undefined && value !== '')
+ .map(([label, value]) => (
+
+ | {label} |
+ {displayScore(value)} |
+
+ ))}
+
+
+
+
+ {semester.comments && Object.keys(semester.comments).length > 0 ? (
+
+
Comments
+
+ {Object.entries(semester.comments).map(([type, text]) => (
+
+
{type.replace(/_/g, ' ')}
+
{text}
+
+ ))}
+
+
+ ) : null}
+
+ ))}
+
+
+
+
+
+
+
+ ) : null}
+
+ )
+}
diff --git a/src/pages/grading/GradingMainPage.tsx b/src/pages/grading/GradingMainPage.tsx
index c597893..c759a65 100644
--- a/src/pages/grading/GradingMainPage.tsx
+++ b/src/pages/grading/GradingMainPage.tsx
@@ -11,6 +11,7 @@ import {
import type { GradingOverviewStudentRow } from '../../api/types'
import {
GRADING_BELOW_SIXTY_PATH,
+ GRADING_DECISIONS_PATH,
GRADING_HOMEWORK_TRACKING_PATH,
GRADING_PARTICIPATION_PATH,
GRADING_PLACEMENT_INDEX_PATH,
@@ -347,6 +348,7 @@ export function GradingMainPage() {
+ Decisions
Below 60 Summary
Homework Tracking
Placement Scores
diff --git a/src/pages/grading/HomeworkTrackingPage.tsx b/src/pages/grading/HomeworkTrackingPage.tsx
index b8f26c8..c77a825 100644
--- a/src/pages/grading/HomeworkTrackingPage.tsx
+++ b/src/pages/grading/HomeworkTrackingPage.tsx
@@ -60,7 +60,7 @@ export function HomeworkTrackingPage() {
fetchHomeworkTracking(searchParams)
.then((p) => {
if (!c) {
- setPayload(p)
+ setPayload(p as HomeworkTrackingPayload)
setError(null)
}
})
diff --git a/src/pages/grading/ParticipationPage.tsx b/src/pages/grading/ParticipationPage.tsx
index 3540eb9..1f4dbab 100644
--- a/src/pages/grading/ParticipationPage.tsx
+++ b/src/pages/grading/ParticipationPage.tsx
@@ -5,6 +5,10 @@ import { fetchParticipation, postParticipation } from '../../api/grading'
import { GRADING_PATH } from './gradingPaths'
/** CI `grading/participation.php` */
+function normalizeParticipationScore(value: unknown): number | string | null {
+ return typeof value === 'number' || typeof value === 'string' ? value : null
+}
+
export function ParticipationPage() {
const [searchParams] = useSearchParams()
const [rows, setRows] = useState<
@@ -17,7 +21,7 @@ export function ParticipationPage() {
}>
>([])
const [locked, setLocked] = useState(false)
- const [title, setTitle] = useState('Participation Scores')
+ const [title] = useState('Participation Scores')
const [sectionName, setSectionName] = useState('')
const [semester, setSemester] = useState(searchParams.get('semester') ?? '')
const [schoolYear, setSchoolYear] = useState(searchParams.get('school_year') ?? '')
@@ -46,7 +50,7 @@ export function ParticipationPage() {
school_id: typeof student.school_id === 'string' ? student.school_id : null,
firstname: typeof student.firstname === 'string' ? student.firstname : null,
lastname: typeof student.lastname === 'string' ? student.lastname : null,
- score: scores.score ?? null,
+ score: normalizeParticipationScore(scores.score),
}
}),
)
@@ -112,7 +116,7 @@ export function ParticipationPage() {
school_id: typeof student.school_id === 'string' ? student.school_id : null,
firstname: typeof student.firstname === 'string' ? student.firstname : null,
lastname: typeof student.lastname === 'string' ? student.lastname : null,
- score: scores.score ?? null,
+ score: normalizeParticipationScore(scores.score),
}
}),
)
diff --git a/src/pages/grading/gradingPaths.ts b/src/pages/grading/gradingPaths.ts
index 6afa90c..d301566 100644
--- a/src/pages/grading/gradingPaths.ts
+++ b/src/pages/grading/gradingPaths.ts
@@ -2,6 +2,8 @@
export const GRADING_PATH = '/app/grading'
/** SPA list + links; Laravel API paths remain `.../below-sixty` (see `api/grading.ts`). */
export const GRADING_BELOW_SIXTY_PATH = `${GRADING_PATH}/below-60`
+export const GRADING_DECISIONS_PATH = `${GRADING_BELOW_SIXTY_PATH}/decisions`
+export const GRADING_BELOW_SIXTY_DECISION_EMAIL_EDITOR_PATH = `${GRADING_DECISIONS_PATH}/email-editor`
export const GRADING_BELOW_SIXTY_EMAIL_EDITOR_PATH = `${GRADING_PATH}/below-60/email-editor`
export const GRADING_HOMEWORK_TRACKING_PATH = `${GRADING_PATH}/homework-tracking`
export const GRADING_PARTICIPATION_PATH = `${GRADING_PATH}/participation`
diff --git a/src/pages/grading/index.ts b/src/pages/grading/index.ts
index c4a7af9..73a3af0 100644
--- a/src/pages/grading/index.ts
+++ b/src/pages/grading/index.ts
@@ -1,5 +1,7 @@
export { BelowSixtyEmailEditorPage } from './BelowSixtyEmailEditorPage'
+export { BelowSixtyDecisionEmailEditorPage } from './BelowSixtyDecisionEmailEditorPage'
export { BelowSixtyPage } from './BelowSixtyPage'
+export { GradingDecisionsPage } from './GradingDecisionsPage'
export { GradingMainPage } from './GradingMainPage'
export { GradingScoreEntryPage } from './GradingScoreEntryPage'
export { HomeworkTrackingPage } from './HomeworkTrackingPage'
diff --git a/src/pages/rolePermission/EditRolePage.tsx b/src/pages/rolePermission/EditRolePage.tsx
index 1dc719e..044c33a 100644
--- a/src/pages/rolePermission/EditRolePage.tsx
+++ b/src/pages/rolePermission/EditRolePage.tsx
@@ -1,6 +1,5 @@
import { type FormEvent, useEffect, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
-import { ApiHttpError } from '../../api/http'
import { fetchRole, updateRole } from '../../api/rolePermission'
import { ROLE_PERMISSION_BASE } from './rolePermissionPaths'