08 — Form Specifications (Leave Module)
- 1. Request Leave Form
- 2. Decision Form (ApprovalActionSheet)
- 3. Create Leave Type Form (org admin)
- 4. Assign Substitution Form (org admin)
- 5. Form system rules (all forms)
Every form the module exposes, its exact DTO contract, validation rules, error mapping, and submission behavior. All contracts are literal copies of the DTO classes; the client mirrors server rules and adds only what the server cannot express (calendar pickers, confirmations).
1. Request Leave Form
Contract: create-leave-request.dto.ts:4-21; submit POST /leave/requests
(leave.controller.ts:32-36).
| Field | Control | Required | Client validation | Server validation (source) |
|---|---|---|---|---|
leaveTypeId | dropdown (from GET /leave/types) | ✓ | non-empty | @IsMongoId() :6-7; type must exist → 404 leave.service.ts:129 |
startDate | date picker (no past restriction) | ✓ | non-empty, valid ISO | @IsDateString() :9-11 |
endDate | date picker | ✓ | ≥ startDate (mirror) | @IsDateString() :13-15; endDate < startDate → 400 leave.service.ts:133-134 |
reason | multiline (≤ 500 chars client) | – | – | @IsString() optional :17-20 |
Server adds (not client-sendable): userId from token (leave.service.ts:138),
daysRequested computed (:136), status: pending (:144).
Read-only preview: days count client-side with the same inclusive formula
(leave.service.ts:310-312) and label "includes weekends/holidays" when the
range contains any.
Error mapping
| HTTP | Source | UI |
|---|---|---|
| 400 | endDate must be on or after startDate. :134 | inline on endDate |
| 404 | Leave type not found. :129 | refetch types + retry |
| 401 | JWT guard leave.controller.ts:27 | session refresh |
| 409 | (no overlap rule exists today — gap) | client warning only |
Submit: disable while in-flight; success → toast + pop; failure → keep form values.
2. Decision Form (ApprovalActionSheet)
Contract: leave-decision.dto.ts:4-17; submit PATCH /leave/requests/:id/approve
(leave.controller.ts:49-53).
| Field | Control | Required | Notes |
|---|---|---|---|
action | segmented approve | reject | ✓ | @IsEnum(LeaveDecisionAction) :11-12; enum :4-7 |
note | multiline (≤ 500 client) | – | @IsOptional() @IsString() :14-17; stored as decisionNote leave.service.ts:199 |
Server rules surfaced in form:
- request must be
pendingelse 409 (leave.service.ts:175-178) → toast "Already decided" + refetch. - cannot decide own request → 409 (
:179-180) — client hides own rows. - approve with insufficient balance → 409 (
:188-189) → block approve, offer reject-with-note.
Confirmation: reject with note → single confirm dialog ("Reject this leave request?"). Approve → immediate (fast path).
3. Create Leave Type Form (org admin)
Contract: create-leave-type.dto.ts:4-28; submit POST /leave/types
(leave.controller.ts:61-65).
| Field | Control | Required | Server rule |
|---|---|---|---|
code | text, uppercase transform | ✓ | @IsString() :6-7; unique per tenant leave-type.schema.ts:29 |
name | text | ✓ | @IsString() :10-11 |
daysPerYear | number stepper | ✓ | @IsInt() @Min(1) :14-16 |
carryForward | switch | – | @IsBoolean() default false :18-21 |
maxCarryForward | number (visible only when carry on) | – | @IsInt() @Min(0) :23-27 |
Duplicate code: Mongo E11000 on unique index → client maps to inline "Code already exists" (no server mapper — gap).
Read-only note: isDefault types cannot be duplicated in practice (code
uniqueness); defaults appear in list automatically (leave.service.ts:299-306).
4. Assign Substitution Form (org admin)
Contract: assign-substitution.dto.ts:4-37; submit POST /leave/substitutions
(leave.controller.ts:73-77).
| Field | Control | Required | Server rule |
|---|---|---|---|
leaveRequestId | hidden (from context) | ✓ | @IsMongoId() :6-7; must exist 404 :225-226; must be approved 409 :227-230 |
substituteTeacherId | teacher search picker | ✓ | @IsMongoId() :10-11; clash check :241-250 |
classId | class picker | ✓ | @IsMongoId() :14-15 |
subjectId | subject picker | ✓ | @IsMongoId() :18-19 |
date | date picker | ✓ | @IsDateString() :22-23; should fall inside approved range (client hint only — gap) |
startTime | time picker HH:mm | ✓ | @IsString() :26-27 |
endTime | time picker HH:mm | ✓ | @IsString() :29-30; start < end (client; implied by timeOverlaps :314-321) |
notes | multiline | – | @IsOptional() @IsString() :34-36 |
Error mapping
| HTTP | Source | UI |
|---|---|---|
| 409 | Leave request must be approved before assigning a substitution. :228-229 | disable form; back to list |
| 404 | No teacher record found for the leave requester. :236-237 | toast, block |
| 409 | Substitute teacher already assigned in this time slot. :247-248 | inline under time fields |
5. Form system rules (all forms)
- Validate on submit + on-field-blur; errors in live regions; focus first invalid.
- Disable submit while loading; never double-submit (idempotency on POSTs is server-side concern — clients retry only on explicit user action).
- Offline: request/decision forms block (they mutate server state); read-only screens degrade to cache.
- All text via i18n keys (00-shared/09); dates localized per user locale.