06 — Screen Specifications (Leave Module)
- 1. Leave Balance Screen
- 2. Request Leave Screen
- 3. My Requests List
- 4. Request Detail Sheet
- 5. Approvals Queue
- 6. Substitutions List (substitute teacher)
- 7. Assign Substitution Sheet (org admin)
- 8. Leave Types (org admin)
- 9. Leave Calendar
- Cross-screen rules
Full behavioral specification per screen: layout, states, interactions, accessibility, motion, adaptive behavior, and edge cases — all anchored to the API contract. Shared motion/a11y baselines: 00-shared/08, 00-shared/09. This is the largest design doc in the package; every rule maps to source.
1. Leave Balance Screen
1.1 Layout (mobile, 360 dp reference)
AppBar: "My Leave Balance" [refresh]
────────────────────────────
[Summary strip] Total remaining (sum of daysRemaining) (proposed)
────────────────────────────
LeaveTypeCard × N (01 = product overview defaults)
[CL] Casual Leave 12 days/year
8 remaining ────── 4 used · 0 carried forward
────────────────────────────
Caption: "Weekends and holidays count as leave days."
1.2 Data contract
GET /leave/balance/:userId → 200 array of
{ leaveTypeId, code, name, daysPerYear, carriedForward, daysUsed, daysRemaining }
(leave.service.ts:66-74,89-124). Sorted as returned (types sorted by
repository default; listTypes uses {code:1} leave.service.ts:216).
1.3 States
| State | Render |
|---|---|
| loading | skeleton AppSkeleton per expected card (default types known: CL, SL, EL, ML, PL — leave.service.ts:30-64) |
| success | cards + progress bars; daysRemaining = daysPerYear + carriedForward − daysUsed (leave.service.ts:121) |
| empty | should not occur (defaults seeded :299-306); fallback AppEmptyState "No leave types configured" |
| error 404 | "User not found" inline (API does not validate user — gap) |
| offline | cached snapshot + AppOfflineBanner; data freshness ≤ 1 session (live-computed :87-88) |
| permission | n/a server-side; client shows own only |
1.4 Interactions & motion
- Pull-to-refresh → refetch;
m-fastopacity swap on refresh (per00-shared/08). - Progress bar animates
m-baseon data arrival. - Tap card → no navigation (or type detail popover
(proposed)).
1.5 Edge cases
- Type with
carryForward=false→ hide "carried forward" line. daysRemainingnegative (over-use impossible by server check,:188-189, but back-dated requests could still makedaysUsedexceed entitlement across months) → clamp progress bar at 0, show "0 remaining" in error color.- Year boundary: balance uses calendar year + previous-year lookback
(
:92-97); on Jan 1 numbers reset — client must not cache across days.
1.6 Accessibility
- Each card:
Semantics(label: "Casual Leave, 8 of 12 days remaining"). - Progress bars expose
valuevia semantics; color is never the only indicator (add percentage text). - Contrast: status colors per
00-shared/02.
2. Request Leave Screen
2.1 Layout
AppBar: "New Leave Request"
[Leave type] ▾ dropdown (from GET /leave/types)
[Start date] date picker
[End date] date picker
[Days] read-only preview: "3 days" (client counts inclusively,
mirror of leave.service.ts:310-312)
[Reason] multiline, optional
────────────────────────────
[Submit request] (filled, fullWidth)
2.2 Form contract (create-leave-request.dto.ts:4-21)
| Field | Type | Rules (source) |
|---|---|---|
leaveTypeId | mongoId | required (:6-7) |
startDate | ISO date string | required (:9-11) |
endDate | ISO date string | required (:13-15); must be ≥ startDate (leave.service.ts:133-134, 400) |
reason | string | optional (:17-20) |
userId and daysRequested come from the server (leave.service.ts:138,136).
2.3 States & validation
| State | Behavior |
|---|---|
| loading types | dropdown disabled with skeleton |
| client validation | end < start → inline error (mirror 400); empty type/date → required errors; daysRequested preview updates live |
| submit | button spinner; disable resubmit |
| 404 | "Leave type not found" → refetch types |
| offline | queued write with idempotency (per 00-shared/01 §8) or block-and-inform — recommend block; balance checks need live data |
| success | toast "Request submitted (pending)"; pop to My Requests |
2.4 Edge cases
- Weekend span: show computed days + caption "includes weekends" (server
counts calendar days,
:310-312). - Overlapping own pending request: no server rule (gap — see
14 §2); client warns "You have a pending request in this range" (best-effort scan of cached own requests). - Past start date: allowed by API; client optional warning.
- Zero-day range: impossible (
endDate ≥ startDate→ ≥ 1 day). - No types: server seeds defaults on first access (
:299-306); if list still empty (rare) showAppEmptyState.
2.5 Accessibility
- Date fields: labels +
Semanticshint "opens date picker"; keyboard entry allowed (yyyy-mm-dd) for power users. - Errors in live region; focus first invalid field.
- min tap target 48 dp for pickers/CTA.
3. My Requests List
3.1 Layout
AppBar: "My Leave Requests" [FAB +]
[Filter chips] All | Pending | Approved | Rejected
────────────────────────────
RequestTile × N
[CL] 12 Aug – 14 Aug · 3 days [pending chip]
"Family function" (reason, 2-line ellipsis)
2 days ago · note: "Approved by Ramesh" (when decidedBy)
3.2 Data contract
GET /leave/requests (own enforced: leave.service.ts:166-167; sorted
createdAt desc :168). Optional ?status= (leave.controller.ts:40).
?userId= is admin-only (:167); non-admin client never sends it.
3.3 States & interactions
| Item | Spec |
|---|---|
| filter chips | server-side refetch per status (AppChips single-select); keep All default |
| row | tap → detail sheet; swipe actions: none (no cancel endpoint) |
| empty | AppEmptyState "No leave requests yet" + CTA "Request leave" |
| pull-to-refresh | refetch current filter |
| offline | cached list + banner; decisions unavailable |
| badge | chip colors: pending → tertiary, approved → success, rejected → error (semantic tokens, 00-shared/02) |
3.4 Edge cases
- Request decided after row rendered → next fetch shows new status; no push
needed for MVP
(planned)notifications. reasonabsent → omit line.- Long reasons → 2-line ellipsis; full text in detail sheet.
4. Request Detail Sheet
Bottom sheet (or master-detail pane on tablet, 00-shared/05).
| Section | Content |
|---|---|
| header | type badge + status chip + daysRequested |
| body | dates, reason, timestamps (createdAt), decision block: decidedBy, decidedAt, decisionNote (leave-request.schema.ts:41-48) |
| actions | none (requester); admin on approved → "Assign substitution" |
Empty decisionNote → "No note provided" muted line.
5. Approvals Queue
5.1 Layout
AppBar: "Leave Approvals" [filter: All | userId (staff picker)]
[Chips] Pending (default) | Approved | Rejected
────────────────────────────
ApprovalTile × N
[SL] Sneha R. · 12 Aug – 14 Aug · 3 days [pending chip]
"Fever, doctor's note attached" (reason)
Balance: 9/10 remaining (contextual, proposed)
[Approve] [Reject]
5.2 Data contract
GET /leave/requests?status=pending (+ optional ?userId=). Admin path
confirmed at leave.service.ts:163-167 via ctx.roles.includes('org_admin').
5.3 Decision flow
- Tap "Reject" →
AppBottomSheetwith note field (optional) + confirm. PATCH /leave/requests/:id/approvebody{action: "approve"|"reject", note?}(leave-decision.dto.ts:9-17).- Response 200 → chip updates; remove row from
pendingfilter. - Errors:
- 409 already
approved/rejected(leave.service.ts:175-178) → toast "Already decided" + refetch list. - 409 insufficient balance (
:188-189) → inline chip "Insufficient balance" on the row; row stays pending; admin may reject with note. - 409 self-decision (
:179-180) → client hides own requests in this screen (server would reject anyway).
- 409 already
- Optimistic update: flip chip to "processing" (neutral), rollback on error —
decision is money-adjacent (balance), so no silent optimistic success;
wait for 200 (see
14 §5race notes).
5.4 Empty & edge
- "No pending requests" with confetti-free calm copy.
- Reject without note allowed (
note?optional,leave-decision.dto.ts:14-17). - Requester names: API returns
userIdonly — client resolves names via users module(forward-looking); until then show short id.
6. Substitutions List (substitute teacher)
AppBar: "My Substitutions"
[Date group header] Mon 18 Aug
[Physics 9A] 08:00–09:00 · for Sneha R. [assigned chip]
notes: "Cover chapter 4"
- Source:
GET /leave/substitutions/teacher/:id(leave.controller.ts:79-83), sorteddateasc (leave.service.ts:275-280); group client-side by date. - States: loading / empty "No substitutions assigned" / error.
- Status chip:
assignedis the only state the service ever sets (leave.service.ts:261);completed|cancelledare schema-defined but unreachable (substitution.schema.ts:7-11) — render onlyassignedfor now. - Refresh: pull-to-refresh; offline → cached + banner.
7. Assign Substitution Sheet (org admin)
Form fields (assign-substitution.dto.ts:4-37):
| Field | Control | Rules |
|---|---|---|
substituteTeacherId | teacher picker (search) | required :9-11 |
classId | class picker | required :13-15 |
subjectId | subject picker | required :17-19 |
date | date picker | required :21-23; must fall within approved leave range (client hint; server does not check — gap) |
startTime/endTime | time pickers (HH:mm) | required :25-31; start < end (client; server clash check implies it :246) |
notes | multiline | optional :33-36 |
Submit → POST /leave/substitutions → 201/200 → toast → open Substitutions.
Errors: 409 not approved (leave.service.ts:227-230), 404 no teacher
record for requester (:232-238), 409 time clash (:241-250, message
"Substitute teacher already assigned in this time slot."). Clash message shown
inline under the time fields.
8. Leave Types (org admin)
8.1 List
GET /leave/types (leave.controller.ts:67-71) — cards: code badge, name,
daysPerYear, carry-forward chips (carryForward + maxCarryForward).
Default types present on first access (leave.service.ts:299-306).
8.2 Create sheet
Fields per create-leave-type.dto.ts:4-28: code (uppercase, 2-4 chars,
hint), name, daysPerYear (@Min(1) :14-16), carryForward switch
(:18-21), maxCarryForward (shown only when carry on, :23-27).
Submit → POST /leave/types; duplicate (tenantId, code) → Mongo
E11000 (unique index leave-type.schema.ts:29) → map to "Code already
exists" inline (server has no mapper — gap).
9. Leave Calendar
AppBar: "Leave Calendar" [‹] Aug 2026 [›]
Mo Tu We Th Fr Sa Su
3 4 5 [6×2] 7 ...
────────────────────────────
Day cell: up to 3 avatar chips + "+2" overflow; tap → list of
approved requests that day (client-side filter of month payload).
- Source:
GET /leave/calendar?from=&to=— approved only, range overlap (leave.service.ts:282-296); default month = server's current month (:283-287). Client always sends explicitfrom/tofor the visible month. - Paging: prev/next month triggers refetch.
- Colors: day with leave → primary-tinted cell; today → outlined.
- Offline: cache last month; show banner.
Cross-screen rules
- All timestamps render in user
timezone(user.schema.ts:52-53) with server dates parsed as UTC. - Every list screen: skeleton → content/empty; errors never blank.
- Envelope: unwrap
{success, message, data, timestamp, requestId}per00-shared/07.