-
-
Decided
-
{metrics.decided}
+
+
+
{stats.totals.Pending}
+
Pending
-
-
-
Year Score Under 50
-
{metrics.urgent}
+
+
+
{stats.totals.Total}
+
Total
- {success ?
{success}
: null}
+ {success ?
{success}
: null}
{error ?
{error}
: null}
{loading ?
Loading decisions…
: null}
{!loading && rows.length === 0 ? (
-
No students below 60 for the whole year in {schoolYear}.
+
No semester scores found for this school year.
) : null}
{!loading && rows.length > 0 ? (
<>
+
+
+
Student Decisions Summary — {schoolYear}
+
+ Trophy counts use the same year-score rule as the trophy final page.
+
+
+
Print Stats
+
+
+
+
+ | Metric |
+ Count |
+ Percent |
+ Boys |
+ Boys % |
+ Girls |
+ Girls % |
+
+
+
+
+ | Total Students |
+ {stats.totals.Total} |
+ 100.0% |
+ {stats.gender.all.boys} |
+ {pct(stats.gender.all.boys, stats.totals.Total)} |
+ {stats.gender.all.girls} |
+ {pct(stats.gender.all.girls, stats.totals.Total)} |
+
+
+ | Pass |
+ {stats.totals.Pass} |
+ {pct(stats.totals.Pass, stats.totals.Total)} |
+ {stats.gender.pass.boys} |
+ {pct(stats.gender.pass.boys, stats.totals.Pass)} |
+ {stats.gender.pass.girls} |
+ {pct(stats.gender.pass.girls, stats.totals.Pass)} |
+
+
+ | Trophies |
+ {stats.totals.Trophy} |
+ {pct(stats.totals.Trophy, stats.totals.Total)} |
+ {stats.gender.trophy.boys} |
+ {pct(stats.gender.trophy.boys, stats.totals.Trophy)} |
+ {stats.gender.trophy.girls} |
+ {pct(stats.gender.trophy.girls, stats.totals.Trophy)} |
+
+
+
+
+
+
- | Student Name |
- Age |
- Class-Section |
+ Student Name |
+ Section |
+ Fall Score |
+ Spring Score |
Year Score |
- Comments / Rationale & Decision |
- Below-60 Decision |
+ Decision |
+ Source |
+ Notes |
{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 yearScore = numericScore(row.year_score)
+ const rowClass = yearScore !== null && yearScore < 60
+ ? (yearScore < 50 ? 'table-danger' : 'table-warning')
+ : ''
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)}`
+ const source = sourceMeta(fieldText(row.source))
return (
| {studentName} |
- {fieldText(row.age) || '—'} |
{fieldText(row.class_section_name) || '—'} |
+ {displayScore(row.fall_score)} |
+ {displayScore(row.spring_score)} |
+ {displayScore(row.year_score)} |
- {displayScore(row.year_score)}
-
- |
-
- |
-
- {decision !== '' && badgeClass ? (
- <>
-
- {decision}
-
-
- Send Email
-
- >
) : (
- Pending
+ —
)}
|
+
+ {source.label}
+ |
+
+ {fieldText(row.notes) || '—'}
+ |
)
})}
-
-
- {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/gradingPaths.ts b/src/pages/grading/gradingPaths.ts
index d301566..5b30623 100644
--- a/src/pages/grading/gradingPaths.ts
+++ b/src/pages/grading/gradingPaths.ts
@@ -2,8 +2,9 @@
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_DECISIONS_PATH = `${GRADING_PATH}/decisions`
+export const GRADING_BELOW_SIXTY_DECISIONS_PATH = `${GRADING_BELOW_SIXTY_PATH}/decisions`
+export const GRADING_BELOW_SIXTY_DECISION_EMAIL_EDITOR_PATH = `${GRADING_BELOW_SIXTY_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 73a3af0..8637e12 100644
--- a/src/pages/grading/index.ts
+++ b/src/pages/grading/index.ts
@@ -1,5 +1,6 @@
export { BelowSixtyEmailEditorPage } from './BelowSixtyEmailEditorPage'
export { BelowSixtyDecisionEmailEditorPage } from './BelowSixtyDecisionEmailEditorPage'
+export { BelowSixtyDecisionsPage } from './BelowSixtyDecisionsPage'
export { BelowSixtyPage } from './BelowSixtyPage'
export { GradingDecisionsPage } from './GradingDecisionsPage'
export { GradingMainPage } from './GradingMainPage'
diff --git a/src/pages/rolePermission/AssignRolePage.tsx b/src/pages/rolePermission/AssignRolePage.tsx
index 02affb5..516fa5a 100644
--- a/src/pages/rolePermission/AssignRolePage.tsx
+++ b/src/pages/rolePermission/AssignRolePage.tsx
@@ -41,7 +41,9 @@ export function AssignRolePage() {
useEffect(() => {
fetchRolesList()
.then((d) => setRoles(d.roles ?? []))
- .catch(() => {})
+ .catch((e: unknown) =>
+ setError(e instanceof ApiHttpError ? e.message : 'Failed to load roles.'),
+ )
}, [])
useEffect(() => {
@@ -283,6 +285,9 @@ export function AssignRolePage() {