Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

06 — Screen Specifications (Leave Module)

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/:userId200 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

StateRender
loadingskeleton AppSkeleton per expected card (default types known: CL, SL, EL, ML, PL — leave.service.ts:30-64)
successcards + progress bars; daysRemaining = daysPerYear + carriedForward − daysUsed (leave.service.ts:121)
emptyshould not occur (defaults seeded :299-306); fallback AppEmptyState "No leave types configured"
error 404"User not found" inline (API does not validate user — gap)
offlinecached snapshot + AppOfflineBanner; data freshness ≤ 1 session (live-computed :87-88)
permissionn/a server-side; client shows own only

1.4 Interactions & motion

  • Pull-to-refresh → refetch; m-fast opacity swap on refresh (per 00-shared/08).
  • Progress bar animates m-base on data arrival.
  • Tap card → no navigation (or type detail popover (proposed)).

1.5 Edge cases

  • Type with carryForward=false → hide "carried forward" line.
  • daysRemaining negative (over-use impossible by server check, :188-189, but back-dated requests could still make daysUsed exceed 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 value via 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)

FieldTypeRules (source)
leaveTypeIdmongoIdrequired (:6-7)
startDateISO date stringrequired (:9-11)
endDateISO date stringrequired (:13-15); must be ≥ startDate (leave.service.ts:133-134, 400)
reasonstringoptional (:17-20)

userId and daysRequested come from the server (leave.service.ts:138,136).

2.3 States & validation

StateBehavior
loading typesdropdown disabled with skeleton
client validationend < start → inline error (mirror 400); empty type/date → required errors; daysRequested preview updates live
submitbutton spinner; disable resubmit
404"Leave type not found" → refetch types
offlinequeued write with idempotency (per 00-shared/01 §8) or block-and-inform — recommend block; balance checks need live data
successtoast "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) show AppEmptyState.

2.5 Accessibility

  • Date fields: labels + Semantics hint "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

ItemSpec
filter chipsserver-side refetch per status (AppChips single-select); keep All default
rowtap → detail sheet; swipe actions: none (no cancel endpoint)
emptyAppEmptyState "No leave requests yet" + CTA "Request leave"
pull-to-refreshrefetch current filter
offlinecached list + banner; decisions unavailable
badgechip 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.
  • reason absent → 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).

SectionContent
headertype badge + status chip + daysRequested
bodydates, reason, timestamps (createdAt), decision block: decidedBy, decidedAt, decisionNote (leave-request.schema.ts:41-48)
actionsnone (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

  1. Tap "Reject" → AppBottomSheet with note field (optional) + confirm.
  2. PATCH /leave/requests/:id/approve body {action: "approve"|"reject", note?} (leave-decision.dto.ts:9-17).
  3. Response 200 → chip updates; remove row from pending filter.
  4. 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).
  5. 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 §5 race 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 userId only — 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), sorted date asc (leave.service.ts:275-280); group client-side by date.
  • States: loading / empty "No substitutions assigned" / error.
  • Status chip: assigned is the only state the service ever sets (leave.service.ts:261); completed|cancelled are schema-defined but unreachable (substitution.schema.ts:7-11) — render only assigned for now.
  • Refresh: pull-to-refresh; offline → cached + banner.

7. Assign Substitution Sheet (org admin)

Form fields (assign-substitution.dto.ts:4-37):

FieldControlRules
substituteTeacherIdteacher picker (search)required :9-11
classIdclass pickerrequired :13-15
subjectIdsubject pickerrequired :17-19
datedate pickerrequired :21-23; must fall within approved leave range (client hint; server does not check — gap)
startTime/endTimetime pickers (HH:mm)required :25-31; start < end (client; server clash check implies it :246)
notesmultilineoptional :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 explicit from/to for 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} per 00-shared/07.