Reporting Pages
The reporting section provides three pages for viewing, managing, and exporting student grades. All reporting pages are class-teacher only -the buttons to access them are hidden from subject teachers, and the pages themselves verify class teacher status before rendering.
Grade data displayed on these pages comes from live calculation endpoints (/calculations/), not from the persisted reporting schema. The reporting schema is used only for metadata (remarks, letter grades, status) and file storage.
Frontend Library Files
All report-related utilities live in lib/reports/ and are barrel-exported via lib/reports/index.ts.
| File | Purpose |
|---|---|
lib/reports/index.ts | Barrel re-export of all report modules |
lib/reports/api.ts | API functions and types for the reporting schema (file uploads, class summary files) |
lib/reports/calculations.ts | API functions for calculation endpoints, types for term/year results, and termResultsToClassSummary converter |
lib/reports/pdf.ts | PDF generation for individual student reports and term-based class summaries (jsPDF) |
lib/reports/year-pdf.ts | PDF generation for year-based student reports and year-based class summaries (jsPDF) |
lib/reports/exam-report-pdf.tsx | Class exam broadsheet PDF matching physical school form (@react-pdf/renderer) |
lib/reports/student-report-pdf.tsx | Individual student report card PDF matching physical school form (@react-pdf/renderer) |
lib/reports/export.ts | CSV and XLSX export for term-based class summaries |
lib/reports/year-export.ts | CSV and XLSX export for year-based class summaries |
lib/grading-rules.ts | Centralized grading model display rules (mirrors backend strategy pattern) |
Reports List Page
Route: /dashboard/classes/[classId]/reports
File: app/dashboard/classes/[classId]/reports/page.tsx
Displays all students in a class with their live calculated grades and the status of any persisted reports.
Filters
| Selector | Source | Notes |
|---|---|---|
| Report Type | Static | Options: "Term" or "Year-end" |
| Term | GET /api/terms?yearId=<id> | Hidden when Report Type is "Year-end". Sorted by sort_order |
Data Flow
- Live grades are fetched from
/calculations/class-term(term reports) or/calculations/class-year(year-end reports) - Persisted reports are fetched from
/reports?studentGroupId=&termId=in parallel - The two are merged into a
MergedStudentlist -live averages with report status overlaid
Student Table
| Column | Description |
|---|---|
| Student | Full name |
| Overall Average | Live calculated average (from calculation endpoint) |
| Report Status | Badge: Draft / Published / Sent to ministry / "Not generated" |
| Actions | "Open" link to report detail (if report exists) |
Generate Reports Button
- Label is "Generate Reports" if no persisted reports exist, "Regenerate Reports" if some exist
- Calls
POST /api/reports/generateto persist current live calculations to thereport_booktable - This is optional -grades display immediately without generating
Student Report Detail Page
Route: /dashboard/classes/[classId]/reports/student?studentId=&termId=&reportType=
File: app/dashboard/classes/[classId]/reports/student/page.tsx
Displays a single student's report with live calculated grades and PDF download. Receives studentId, termId, and reportType via URL query parameters (no persisted report ID needed).
Data Sources
| Data | Source | Purpose |
|---|---|---|
| Live term grades | GET /api/calculations/student-term | CW, Exam, Term composite for each subject |
| Live year grades | GET /api/calculations/student-year | Year-end: per-term composites, year grade |
Page Header
- Student name
- Term name
- Class ranking (from live data)
Subject Results Table
Displays differently based on report type:
Term Report
| Column | Description |
|---|---|
| Subject | Subject name |
| Coursework | Coursework average |
| Exam | Exam average |
| Term | Term composite |
Year-End Report - Per-Term Layout (weighted_continuous)
| Column | Description |
|---|---|
| Subject | Subject name |
| M, H, T (term initials) | Per-term composite grades |
| End of Yr Exam | Last term's exam average |
| Year Grade | Computed year grade |
Year-End Report - Pooled Layout (weighted_cumulative, continuous_cumulative)
| Column | Description |
|---|---|
| Subject | Subject name |
| AVG /cwWeight | Average of term composites, scaled to coursework weight |
| FINAL /exWeight | Final exam from last term, scaled to exam weight (continuous_cumulative only) |
| Total | Year grade |
The layout is driven by getGradingRules(model).display.yearEndColumns - either "per_term" or "pooled".
PDF Downloads
Two PDF options:
| Button | Library | Description |
|---|---|---|
| Download PDF | jsPDF | Simple tabular PDF (Subject / CW / Exam / Term columns) |
| Report Card | @react-pdf/renderer | Styled report card matching physical school form (header info block + grades table + signature lines) |
The report card PDF (buildStudentReportPdfBlob) includes:
- Student name, term (uppercased), class, position, overall average
- Attendance fields (possible attendance, times absent)
- Grades table: SUBJECT / COURSE WORK % / Final Exam % / Total / GRADE (letter)
- Summary row with total and overall average
- Signature lines for Class Teacher and Principal
Class Report Page
Route: /dashboard/classes/[classId]/class-report
File: app/dashboard/classes/[classId]/class-report/page.tsx
Class-level summary with statistics, rankings, and export capabilities.
Filters
| Selector | Position | Notes |
|---|---|---|
| Report Type | Left | Options: "Term" or "Year-end" |
| Term | Right | Hidden when "Year-end" is selected |
Data Flow
- Term reports:
GET /api/calculations/class-term→ converted toClassSummaryviatermResultsToClassSummary() - Year-end reports:
GET /api/calculations/class-year+GET /api/calculations/class-termin parallel - Stored files are fetched from
/reports/class-summary/files
Statistics Cards
| Card | Description |
|---|---|
| Class Average | Mean of all student overall averages |
| Highest / Lowest | Best and worst overall averages |
| Students | Total count with pass/fail breakdown (threshold: 50) |
| Weights | Coursework and exam weight percentages |
Subject Averages Table (term-based only)
| Column | Description |
|---|---|
| Subject | Subject name |
| Average | Class average for this subject |
| Highest | Highest individual mark |
| Lowest | Lowest individual mark |
Student Rankings Table
Term View
| Column | Description |
|---|---|
| # | Position |
| Student | Full name |
| Overall Average | Term composite average |
Year-End View - Per-Term Models (weighted_continuous)
| Column | Description |
|---|---|
| # | Position |
| Student | Full name |
| Per-subject: M, H, T, E, Yr | Term composites, exam, year grade per subject |
| Year Average | Year-end overall average |
Year-End View - Pooled Models (weighted_cumulative, continuous_cumulative)
| Column | Description |
|---|---|
| # | Position |
| Student | Full name |
| Per-subject: CA, Exam, Total | Scaled CA, exam, and total per subject |
| Year Average | Year-end overall average |
Export Section
Four download formats, all generated client-side from live calculation data:
| Format | Term-based | Year-based |
|---|---|---|
buildClassSummaryPdfBlob -CW/EX/Final per subject, with subject separator lines | buildYearClassSummaryPdfBlob -term initials + End of Yr Exam + Year per subject | |
| Exam Report Card | buildEndOfYearExamPdfBlob -class broadsheet matching physical school exam form (@react-pdf/renderer) | Same, with scoreField: "yearGrade" |
| CSV | buildClassSummaryCsv -same column structure as PDF | buildYearClassSummaryCsv -matches year PDF layout |
| XLSX | buildClassSummaryXlsx -two sheets (Summary + Students) with merged subject headers | buildYearClassSummaryXlsx -matches year PDF layout |
All exports include:
- Class name and term name in the header
- Weight information (CW% / EX%)
- Aggregate statistics (class average, pass/fail)
The Generate & save all to storage button creates all three formats, downloads them, and uploads to Supabase Storage.
Dynamic Page Width (PDF)
Both term-based and year-based class summary PDFs dynamically calculate page width based on the number of subjects. If the table exceeds standard A4 landscape width (297mm), the page extends horizontally to fit all columns.
PDF Generation Details
Individual Student PDF (buildReportPdfBlob)
Takes a StudentTermResult (live data) and optional ReportDetail (persisted metadata). Includes:
- Student name, term, report type, status
- Overall average and class position
- Conduct, attendance, class teacher remark (from persisted data)
- Subject table: CW, Exam, Term, Letter Grade, Remark
Year-End Student PDF (buildYearReportPdfBlob)
Takes a StudentYearReport. Includes:
- Student name, class name
- Subject table with term initials as column headers (e.g., M, H, T)
- End of Yr Exam column (last term's exam average)
- Year Grade column
- OVERALL summary row
- Legend mapping initials to full term names
Class Summary PDF (buildClassSummaryPdfBlob)
Takes a ClassSummary. Includes:
- Header with class name, term, weights, student count, averages
- Subject averages table
- Student grades table with two-row header: subject names (merged) + CW/EX/Final sub-columns
- Vertical separator lines between subject groups
Year-End Class Summary PDF (buildYearClassSummaryPdfBlob)
Takes StudentYearReport[]. Includes:
- Header with class name, academic year, weights
- Student table with term initials + E (End of Yr Exam) + Year per subject
- Smaller text (5pt) and condensed layout to fit more columns
- Legend for term initials
Exam Broadsheet PDF (buildEndOfYearExamPdfBlob) - @react-pdf/renderer
Takes a ClassSummary and ExamReportOptions. Generates a landscape A4 PDF matching the physical school exam form.
Default Layout (per-term models / term view)
- Title: "END OF YEAR EXAMINATIONS" (configurable)
- Subtitle: "SUBJECTS : Marks out of 100%"
- Grid table: Student's Name | subject columns (vertical headers) | TOTAL | AVE. | Position
- Configurable
scoreField:termComposite,yearGrade, orexamAverage
Pooled Layout (added 2026-05-17)
When yearResults and gradingModel are provided and the model has yearEndColumns: "pooled", the broadsheet uses a two-row header with per-subject sub-columns:
- Row 1: Subject names spanning across sub-columns
- Row 2:
AVG /cwW|FINAL /exW|Totalunder each subject - Subtitle:
CA (40%) + Exam (60%) = Total(using actual weights) - Data: scaled CA average, scaled final exam, year grade per subject + overall TOTAL, AVE., Position
ExamReportOptions accepts optional yearResults (array of year report data) and gradingModel to activate this layout.
Also available as a React component (EndOfYearExamDocument) for use with <PDFViewer>.
Student Report Card PDF (buildStudentReportPdfBlob) - @react-pdf/renderer
Takes a StudentTermResult and StudentReportOptions. Generates a portrait A4 PDF matching the physical school report card. Includes:
- Student name
- Info header block with bordered field pairs: TERM / YEAR, CLASS / NO. IN CLASS, POSITION / OVERALL AVERAGE, POSSIBLE ATTENDANCE / TIMES ABSENT
- Grades table: SUBJECT / COURSE WORK % / Final Exam % / Total / GRADE (letter grade A–F)
- Summary row with total and overall average
- Signature lines for Class Teacher and Principal
Also available as a React component (StudentReportDocument).
Class Detail Page - Generate Report Button
The Class Summary card on the class detail page ([classId]/page.tsx) includes a Generate Report button that produces the exam broadsheet PDF:
- Term view: generates using
termCompositescores for the selected term - Year view: generates using
yearGradescores, passesyearResultsandgradingModelfor pooled layout - Disabled when no data is available or while generating
Grading Rules System (added 2026-05-17)
File: lib/grading-rules.ts
A centralized configuration system that drives all grading-model-specific UI logic. Each grading model has a GradingRules object that controls:
| Property | Type | Purpose |
|---|---|---|
termHasExam | boolean | Whether term views show an exam column |
termHasCoursework | boolean | Whether terms have coursework |
yearAggregation | enum | How year-end grades are aggregated |
display.termCwScaledToYearWeight | boolean | Scale term CW to year weight in year views |
display.examColumnLocation | enum | "each_term" or "final_term_only" |
display.examScaledToYearWeight | boolean | Scale exam to year weight in year views |
display.showTermAvgColumn | boolean | Show AVG column in year views |
display.yearEndColumns | enum | "per_term" or "pooled" - controls report layout |
Usage: getGradingRules(gradingModel) returns the rules for any model. All report generators, PDF builders, CSV/XLSX exports, and UI components use this instead of scattered if (model === "...") checks.
To add a new grading model:
- Add the slug to
GradingModeltype - Create a
GradingRulesentry - Register in
GRADING_RULESmap - Add a label in
GRADING_MODEL_LABELS
API Calls Summary
| Action | Endpoint |
|---|---|
| Live term grades (student) | GET /api/calculations/student-term |
| Live year grades (student) | GET /api/calculations/student-year |
| Live term grades (class) | GET /api/calculations/class-term |
| Live year grades (class) | GET /api/calculations/class-year |
| List persisted reports | GET /api/reports?studentGroupId=&termId= |
| Get report detail | GET /api/reports/:id |
| Generate reports | POST /api/reports/generate |
| Update report | PATCH /api/reports/:id |
| Regenerate report | PATCH /api/reports/:id/regenerate |
| Publish report | PATCH /api/reports/:id/publish |
| Send to ministry | PATCH /api/reports/:id/send-to-ministry |
| Update entry (letter/remark) | PATCH /api/report-entries/:entryId |
| Upload student PDF | POST /api/reports/:id/pdf/upload |
| Download student PDF | GET /api/reports/:id/pdf/:pdfId/download |
| PDF history | GET /api/reports/:id/pdfs |
| Upload class summary file | POST /api/reports/class-summary/upload |
| Download class summary file | GET /api/reports/class-summary/download |
| List class summary files | GET /api/reports/class-summary/files |
| Academic year info | GET /api/academic-years/:id |
| Terms | GET /api/terms?yearId= |