08 — Form Specifications (Students Module)
- 1. Create Student (wizard step 2–4 →
POST /students) - 2. Enroll (
POST /students/:id/enroll) - 3. Transfer (
POST /students/:id/transfer) - 4. Update student (
PATCH /students/:id) - 5. Upload document (
POST /students/:id/documents) - 6. CSV bulk import (
POST /bulk/import/students) - 7. Create parent (
POST /parents) - 8. Link parent (
POST /parents/link/:studentId) - 9. Avatar upload (
POST /users/:id/avatar) - Validation UX rules (all forms)
Every field of every form, exactly mapped to the server DTOs. Types/validation from the DTO files (class-validator decorators); enums from schemas. All fields not listed do not exist server-side.
*= required by backend.
1. Create Student (wizard step 2–4 → POST /students)
Payload DTO: create-student.dto.ts. Identity step targets POST /users
(users.controller.ts:36-40).
| Field | Type / rules (server) | Source | Client input | Notes |
|---|---|---|---|---|
userId* | MongoId | create-student.dto.ts:6-7 | hidden (from step 1 user) | must exist; user created/reused first |
admissionNumber* | string | create-student.dto.ts:9-11 | AppTextField (uppercase, no spaces) | unique per tenant → 409 (student.service.ts:56-62); client pre-check against roster |
rollNumber? | string | create-student.dto.ts:13-16 | text, mono | |
academicYearId* | MongoId | create-student.dto.ts:18-20 | AppDropdown (academic years) | |
campusId? | MongoId | create-student.dto.ts:22-25 | AppDropdown | |
gradeId* | MongoId | create-student.dto.ts:27-29 | AppDropdown | |
sectionId* | MongoId | create-student.dto.ts:31-33 | AppDropdown (by grade) | |
classId* | MongoId | create-student.dto.ts:35-37 | AppDropdown (by grade+section+year) | auto-enrolled here (student.service.ts:71-78) |
houseId? | MongoId | create-student.dto.ts:39-42 | AppDropdown | |
admissionDate? | ISO date string | create-student.dto.ts:44-47 | AppDatePicker (≤ today) | omitted → today (student.service.ts:66-68) |
admissionType? | string enum new|transfer|promoted | create-student.dto.ts:49-52; student.schema.ts:47 | segmented buttons | free string accepted by DTO; UI constrains |
transportRequired? | boolean | create-student.dto.ts:54-56 | AppSwitch | schema default false |
hostelRequired? | boolean | create-student.dto.ts:57-59 | AppSwitch | schema default false |
medicalNotes? | string | create-student.dto.ts:61-65 | multiline |
Identity step (POST /users) — user schema fields (user.schema.ts:16-35):
firstName* (required), middleName?, lastName* (required), displayName?,
email* (unique per tenant → 409; lowercase), phone?. UI submits only
firstName, lastName, email, phone?.
Server status on create: always active (student.service.ts:65), admissionType
default new, enrollment ACTIVE with joinedAt = now.
2. Enroll (POST /students/:id/enroll)
DTO: enroll-student.dto.ts.
| Field | Type / rules | Source |
|---|---|---|
classId* | MongoId | enroll-student.dto.ts:5-7 |
academicYearId* | MongoId | enroll-student.dto.ts:9-11 |
rollNumber? | string | enroll-student.dto.ts:13-16 |
Side effect banner: existing ACTIVE enrollments → transferred + leftAt
(student.service.ts:125-139).
3. Transfer (POST /students/:id/transfer)
DTO: transfer-student.dto.ts.
| Field | Type / rules | Source | Client |
|---|---|---|---|
classId* | MongoId | transfer-student.dto.ts:5-7 | dropdown |
academicYearId* | MongoId | transfer-student.dto.ts:9-11 | dropdown |
gradeId? | MongoId — "defaults to class grade" | transfer-student.dto.ts:13-19 | dropdown, prefilled from class |
sectionId? | MongoId — "defaults to class section" | transfer-student.dto.ts:20-25 | dropdown, prefilled from class |
rollNumber? | string | transfer-student.dto.ts:27-30 | text |
Guard: student status must be active (student.service.ts:175-179).
4. Update student (PATCH /students/:id)
DTO: update-student.dto.ts — every field optional mirror of create plus:
status? (plain string, no enum validation — update-student.dto.ts:60-63;
UI must only send valid StudentStatus values), metadata? (object,
update-student.dto.ts:78-80). Note: PATCH with $set writes only provided
fields (student.service.ts:144). Client edit form = create fields minus class
path (class changes go through transfer/enroll) plus status only via lifecycle
actions, not the form.
5. Upload document (POST /students/:id/documents)
| Part | Type | Source |
|---|---|---|
file* | multipart file — field name exactly file | student.controller.ts:83; MulterFile{buffer,originalname,mimetype,size} |
category? | string (body, multipart text field) | upload-student-document.dto.ts:4-9 |
Server has no size/mime limits (OQ-9). Client (proposed) limits: ≤ 10 MB,
mime ∈ {pdf, jpg, png, webp}. uploadedBy = actor userId (student.service.ts:269).
6. CSV bulk import (POST /bulk/import/students)
Adapter columns (students-import.adapter.ts:17-26):
| CSV column | Required | Validation / resolution | Error string (exact) |
|---|---|---|---|
firstName | ✓ | passed to user create | "Missing required column "firstName"." |
lastName | ✓ | passed to user create | "Missing required column "lastName"." |
email | ✓ | regex ^[^\s@]+@[^\s@]+\.[^\s@]+$ + uniqueness | "Invalid email format." / "Email "{email}" already registered." |
admissionNumber | ✓ | tenant-unique | "Missing required column "admissionNumber"." / "Admission number "{n}" already exists." |
grade | ✓ | matched by name or code (students-import.adapter.ts:118-120) | "Grade "{g}" not found." |
section | ✓ | matched by name | "Section "{s}" not found." |
academicYear | ✓ | matched by name | "Academic year "{y}" not found." |
rollNumber | — | passthrough | — |
Derived resolution: class = first class matching {gradeId, sectionId, academicYearId}
(students-import.adapter.ts:131-141) — error "No class found for grade "{g}"
section "{s}".".
Parsing rules (bulk-import.service.ts:26-35): header row required (first row
= column names), skip_empty_lines, trim, columns: true. Empty file → 400
"CSV must include a header row and data." Malformed → 400 "Malformed CSV: could not
parse file." Row numbering: index + 2 (header = row 1).
Export template (GET /bulk/export/students, students-import.adapter.ts:87-98):
columns admissionNumber, rollNumber, status, admissionDate — sorted by
admissionNumber asc. This is an export format, not an import template —
flagged in UI (import columns differ).
7. Create parent (POST /parents)
DTO: create-parent.dto.ts.
| Field | Type / rules | Source |
|---|---|---|
userId* | MongoId | create-parent.dto.ts:5-7 |
occupation? | string | create-parent.dto.ts:9-12 |
company? | string | create-parent.dto.ts:13-16 |
annualIncome? | number | create-parent.dto.ts:17-20 |
relationshipNotes? | string | create-parent.dto.ts:21-24 |
emergencyContactPriority? | number | create-parent.dto.ts:25-28 |
pickupAuthorization? | boolean | create-parent.dto.ts:29-32 |
Guard: one parent profile per userId → 409 "Parent profile already exists for
this user." (parent.service.ts:30-34).
8. Link parent (POST /parents/link/:studentId)
DTO: link-parent.dto.ts.
| Field | Type / rules | Source |
|---|---|---|
parentId* | MongoId | link-parent.dto.ts:5-7 |
relationship* | string enum: mother|father|guardian|grandparent|relative|foster_parent | link-parent.dto.ts:9-20; student-parent-link.schema.ts:7-14 |
isPrimaryGuardian? | boolean (schema default false) | link-parent.dto.ts:22-24 |
financialResponsibility? | boolean (default false) | link-parent.dto.ts:26-28 |
pickupAllowed? | boolean (schema default true) | link-parent.dto.ts:30-32 |
emergencyPriority? | number (schema default 0) | link-parent.dto.ts:34-36 |
Student existence checked (student-parent-link.service.ts:27); parent existence
not checked (OQ-11).
9. Avatar upload (POST /users/:id/avatar)
Multipart field file (users.controller.ts:96); updates avatarFileId on the
user doc (user.schema.ts:34-35).
Validation UX rules (all forms)
- Run
validatoron submit + after first error; clear on edit (00-shared/03 B). - 400
VALIDATION_ERROR→ mapdetails[].fieldto fields (00-shared/06 §5); focus first invalid (00-shared/09 §10). - 409 → inline conflict with suggested action (search existing / change admission number).
- Submit buttons: disabled while pending (anti-double-submit, 00-shared/08 §6).