08 — Form Specifications (Academics Module)
- 1. Academic Year — Create (
POST /academic-years) - 2. Academic Year — Update (
PATCH /academic-years/:id) - 3. Grade — Create (
POST /grades) - 4. Grade — Update (
PATCH /grades/:id) - 5. Section — Create (
POST /sections) - 6. Section — Update (
PATCH /sections/:id) - 7. Class — Create (
POST /classes) - 8. Class — Update (
PATCH /classes/:id) - 9. Subject — Create (
POST /subjects) - 10. Subject — Update (
PATCH /subjects/:id) - 11. Subject Assignment — Create (
POST /subject-assignments) - Shared submission behaviour (all forms)
Field-by-field specs for every create/update form. Field metadata (required, type, validation, defaults) is exact from
src/modules/academics/dto/*; server validation via class-validator (ValidationPipe→ 400VALIDATION_ERRORwith per-field details, 00-shared/07 §3). Shared form rules: full-screen page (> 3 fields), submit button loading state (anti-double-submit), errors inline per field + focus first invalid, offline → blocked with banner (00-shared/08 §6, 00-shared/10 §2). Dates:IsDateString= ISO-8601YYYY-MM-DD(swagger examples,create-academic-year.dto.ts:9-15).
1. Academic Year — Create (POST /academic-years)
Fields exact from create-academic-year.dto.ts:4-21:
| Field | Req | Control | Validation (server) | Notes / UI |
|---|---|---|---|---|
name | ✅ | AppTextField | IsString | e.g. "2026-2027"; 409 on duplicate (academic-year.service.ts:25-29) — inline banner, server copy |
startDate | ✅ | AppDatePicker | IsDateString | ISO date; helper "e.g. 2026-04-01" |
endDate | ✅ | AppDatePicker | IsDateString | client rule: must be > startDate (server does not check — OQ-5 01; still block client-side) |
status | ⬜ | AppDropdown | IsString | enum values upcoming/active/archived (academic-year.schema.ts:7-11); omitted → server defaults upcoming (academic-year.service.ts:30-33); keep optional, default "Upcoming" |
- Success: 200 doc → navigate to detail; snackbar "Year created".
2. Academic Year — Update (PATCH /academic-years/:id)
From update-academic-year.dto.ts:4-28: all optional — name, startDate,
endDate, status, isCurrent (bool).
- UI rule:
isCurrentis not shown as a normal checkbox — edits use "Set as current" on the list/detail (PATCH /:id/set-current,academic-year.service.ts:69-83) which also forcesstatus: active. Inline checkbox would fight that; keep the dedicated action. - Server sets only provided fields (
$set,academic-year.service.ts:64).
3. Grade — Create (POST /grades)
From create-grade.dto.ts:4-24:
| Field | Req | Control | Validation | Notes |
|---|---|---|---|---|
name | ✅ | AppTextField | IsString | e.g. "Grade 1"; 409 on duplicate (grade.service.ts:21-24) |
academicYearId | ⬜ | AppDropdown (year) | IsMongoId | schema optional (grade.schema.ts:9-10) — label "Attach to year (optional)"; empty allowed, sent as null/omitted |
code | ⬜ | AppTextField | IsString | e.g. "G1"; auto-uppercase formatter |
displayOrder | ⬜ | AppTextField (number) | IsInt, Min(0), default 0 | ladder position; default = next max+1 (client pre-fill, editable) |
- Success → list refresh (keeps
displayOrdersort,grade.service.ts:39).
4. Grade — Update (PATCH /grades/:id)
From update-grade.dto.ts:4-30: all optional — academicYearId, name, code,
displayOrder, status. status shown only for admin; free-string field with a
suggested picker of active / archived (schema default 'active',
grade.schema.ts:21-22; OQ-2 01).
5. Section — Create (POST /sections)
From create-section.dto.ts:4-28:
| Field | Req | Control | Validation | Notes |
|---|---|---|---|---|
gradeId | ✅ | AppDropdown (grade) | IsMongoId | pre-selected when arriving from grade detail |
name | ✅ | AppTextField | IsString | e.g. "A"; client warns if (gradeId, name) duplicates (server does not check — OQ-4) |
capacity | ⬜ | AppTextField (number) | IsInt, Min(1), default 40 | |
classTeacherId | ⬜ | AppDropdown (users, teacher role) | IsMongoId | label "Class teacher (user)" — create-section.dto.ts:19-21 |
roomId | ⬜ | AppTextField | IsString | free text (OQ-6 01) |
6. Section — Update (PATCH /sections/:id)
From update-section.dto.ts:4-35: all optional — gradeId, name, capacity,
classTeacherId, roomId, status. Reparenting caution: changing gradeId
orphans classes referencing this section (class.schema.ts:18-19 required) —
client shows confirm "Classes of this section keep pointing at it".
7. Class — Create (POST /classes)
From create-class.dto.ts:4-41:
| Field | Req | Control | Validation | Notes |
|---|---|---|---|---|
academicYearId | ✅ | AppDropdown (year) | IsMongoId | cascading picker part 1 (07 §6) |
gradeId | ✅ | AppDropdown (grade) | IsMongoId | part 2; disabled until year picked (UI only — server doesn't cross-check refs) |
sectionId | ✅ | AppDropdown (section, filtered by grade) | IsMongoId | part 3; disabled until grade picked |
name | ✅ | AppTextField | IsString | auto-suggest "Grade {grade.name} - {section.name}" (editable) — example "Grade 10 - A" (create-class.dto.ts:22) |
capacity | ⬜ | AppTextField (number) | IsInt, Min(1), default 40 | |
campusId | ⬜ | AppDropdown (campus) | IsMongoId | hidden (planned) — no campus module/endpoint; schema ref points at Class (OQ-6) |
classTeacherId | ⬜ | AppDropdown (users, teacher role) | IsMongoId | "Class teacher (user)" — create-class.dto.ts:32-35 |
roomId | ⬜ | AppTextField | IsString | free text (OQ-6) |
- No uniqueness on
(academicYearId, gradeId, sectionId, name)(class.service.ts:16-18) — client shows a "Duplicate class?" confirm when the exact combo already exists in cache (OQ-4).
8. Class — Update (PATCH /classes/:id)
From update-class.dto.ts:4-50: all optional — academicYearId, campusId,
gradeId, sectionId, name, capacity, classTeacherId, roomId, status.
Same reparenting caution as section (below, §6).
9. Subject — Create (POST /subjects)
From create-subject.dto.ts:4-47:
| Field | Req | Control | Validation | Notes |
|---|---|---|---|---|
code | ✅ | AppTextField | IsString | e.g. "MATH101"; 409 on duplicate (subject.service.ts:21-25) + DB unique index (subject.schema.ts:38); auto-uppercase |
name | ✅ | AppTextField | IsString | e.g. "Mathematics" |
shortName | ⬜ | AppTextField | IsString | e.g. "Maths" |
credits | ⬜ | AppTextField (number) | IsInt, Min(0), default 0 | |
maximumMarks | ⬜ | AppTextField (number) | IsInt, Min(1), default 100 | |
passingMarks | ⬜ | AppTextField (number) | IsInt, Min(1), default 33 | |
theoryMarks | ⬜ | AppTextField (number) | IsInt, Min(0), default 80 | |
practicalMarks | ⬜ | AppTextField (number) | IsInt, Min(0), default 20 |
- Client cross-validation (server has none — OQ-5 01):
theoryMarks + practicalMarks ≤ maximumMarks;passingMarks ≤ maximumMarks; error under the offending field, block submit.
10. Subject — Update (PATCH /subjects/:id)
From update-subject.dto.ts:4-42: all optional — code, name, shortName,
credits, maximumMarks, passingMarks, status.
theoryMarks / practicalMarks are NOT updatable — do not render those fields in
edit mode; add a helper line "Theory/Practical marks locked (planned)" (OQ-5).
code change → 409 if taken (service checks only on create; a duplicate code
update is not checked and will hit the unique index → 500-mapped 409 by the
exception filter — client surfaces as "Code already in use" on 409/5xx
DUPLICATE per http-exception.filter.ts mapping; flag OQ-2 01).
11. Subject Assignment — Create (POST /subject-assignments)
From create-subject-assignment.dto.ts:4-20:
| Field | Req | Control | Validation | Notes |
|---|---|---|---|---|
classId | ✅ | (context, hidden) | IsMongoId | from the class screen — not user-entered |
academicYearId | ✅ | (context, hidden) | IsMongoId | from year switcher |
subjectId | ✅ | AppDropdown (subjects, searchable) | IsMongoId | already-assigned subjects disabled (client guard, OQ-4) |
teacherId | ✅ | AppDropdown (users, teacher role, searchable) | IsMongoId | (planned) dedicated teachers list |
No update form (no endpoint) — "Replace" flow = confirm dialog → DELETE /subject-assignments/:id → reopen create sheet.
Shared submission behaviour (all forms)
- Submit → button
loading(label replaced, 00-shared/08 §6); fields disabled. - 400 → per-field
errorTextfromerror.details[](field names match DTO keys); focus first invalid. - 401 → silent refresh then retry once; fail → sessionExpired.
- 409 → inline
AppBanner(error)with server message (name/code conflicts). - 429 → countdown banner; no auto-retry (00-shared/07 §4).
- 5xx → generic error +
requestId(never server internals). - Success → snackbar + navigate (list or detail per entity); no optimistic write (server returns canonical doc — reconcile from response).