06 — Screen Specifications (Parents Module)
- 1. Parents List
/parents - 2. Parent Detail
/parents/:id - 3. Create / Edit Parent Form
/parents/new,/parents/:id/edit - 4. Link Guardian Sheet (from detail / student screen)
- 5. Unlink Dialog
- 6. My Children
/my/children(forward-looking) - 7. Child Switcher (component)
- 8. My Profile
/my/profile(forward-looking) - Shared micro-interaction & motion notes (all screens)
Production-grade specs for each Parents screen: chrome, states, components, micro-interactions, a11y, responsive, dark/light, keyboard, motion. Tokens:
Sp= 00-shared/02 §3,Ty= typography §2,El= elevation §5,R= radius §4,Mo= motion §6. Components reference 00-shared/03 + 07_Component_Library.md.
1. Parents List /parents
Chrome: standard shell AppBar (headlineMedium "Guardians", subtitle = count from
meta.totalItems); horizontal padding 16 phone / 24 tablet+; AppBar elevation e-1.
Layout (phone):
AppBar "Guardians" headlineMedium
AppSearchBar (client-side filter only — server ignores q, OQ-7)
[AppListTile ×N] ParentListTile: AppAvatar 40, name titleMedium,
subtitle "occupation · company",
trailing children-count badge
infinite scroll footer "End of list" bodySmall / spinner
FAB "Add guardian" FloatingActionButton.extended, Icons.person_add
States:
- initial →
AppSkeleton(list)(8 rows, shimmerm-slow; reduced-motion static tint). - success+data → list;
RefreshIndicator(bypasses cache). - success+empty →
AppEmptyStateIcons.groups, "No guardians yet", action "Add first guardian". - error →
AppErrorState(code); 5xx generic + requestId hint; Retry re-fetches page 1. - offline →
AppOfflineBannertop; stale-while-revalidate from last-good cache (00-shared/06 §3.3). - permission
(planned)→ 403 screen; today any JWT can list (OQ-8).
Pagination: ParentsCubit.loadMore() on scroll end when meta.hasNext
(pagination-query.dto.ts:32-39); bottom spinner fade-in m-instant; page increments
1-based; limit 20 default (pagination-query.dto.ts:19).
Motion: row entrance stagger m-entrance (40 ms); FAB hide on scroll-down, show on
scroll-up (00-shared/03 D).
A11y: search results announced via live region; row semantics "Guardian
Tablet/desktop: ≥ 840 dp master-detail — list pane 320–360 dp, detail in right pane
(see §2); hover row tint m-fast; N key = new guardian; Esc clears search.
Dark/light: token-only (surface, surfaceContainerLow, onSurfaceVariant);
children-count badge primaryContainer.
2. Parent Detail /parents/:id
Chrome: AppBar title = display name (from joined User), back to list; body scrolls.
Layout (phone):
Header card AppCard e-1
AppAvatar 64 (User.avatarFileId / initials)
name titleLarge
occupation · company bodyMedium onSurfaceVariant
AppInfoRow: annual income, relationship notes, emergency priority, pickup
actions: [Edit] OutlinedButton → /parents/:id/edit
Section "Linked children" AppSectionHeader (+ "Link" TextButton)
LinkedChildCard ×N (see component spec §2 of 07)
empty → AppEmptyState "No linked children — Link a student"
States:
- loading →
AppSkeletonheader card + 2 child cards. - 404 →
AppErrorState"Guardian not found" (parent.service.ts:51) + back button. - error →
AppErrorState(code)+ Retry. - children load (
GET /parents/:id/students) is a second call (parent.service.ts:69-72): header renders first, children section shows inlineAppSkeletonrows; failure in children section only → inlineAppErrorState(compact).
Linked children rendering (join, OQ-10): each card fetches GET /students/:id
(client-side join). While joining: AppSkeleton inline. On child 404 (deleted) → card
shows "student no longer active" state with unlink action still available.
Per-child menu (AppMenu):
- "Set as primary" → shown when
isPrimaryGuardianis false; action = delete old primary link + recreate this one (no link PATCH endpoint — OQ-4); confirm dialog explains the two-step. - "Unlink" →
AppDialog(§5) →DELETE /parents/link/:linkId→ row fade-outm-base+ snackbar "Guardian unlinked" (student-parent-link.service.ts:38-41).
Guardian-relationship presentation: RelationshipChip per child + PrimaryGuardianBadge
"Primary" (primaryContainer tonal) when isPrimaryGuardian (student-parent-link.schema.ts:27-29).
Motion: hero AppAvatar list→detail m-slow; card entrances m-entrance stagger.
A11y: sections use Semantics(headers:); menu items labeled; badge announced
"Primary guardian".
Tablet/desktop: master-detail right pane; edit opens as overlay page in pane.
3. Create / Edit Parent Form /parents/new, /parents/:id/edit
Chrome: full-screen page (00-shared/05 §5 — > 3 fields); AppBar title
"Add guardian" / "Edit guardian"; scroll + keyboard avoidance.
Create layout (phone):
User picker (create only) AppTextField(read-only) + search sheet over GET /users?q=
— shows User.displayName + email (users.service.ts:90-115)
occupation AppTextField
company AppTextField
annualIncome AppTextField (number, currency locale)
relationshipNotes AppTextField multiline
emergencyContactPriority AppTextField (number 0-99)
pickupAuthorization AppSwitch ListTile "Authorized to pick up child"
(default false — parent.schema.ts:28)
CTA "Save guardian" FilledButton fullWidth h 48
Edit layout: same minus user picker (UpdateParentDto has no userId,
update-parent.dto.ts:4-34).
Validation (client, mirrors DTO):
userIdrequired + MongoId (create-parent.dto.ts:6-7) — client always passes a picked user's id.- occupation/company/relationshipNotes strings (
IsStringoptional). - annualIncome / emergencyContactPriority numbers (client:
NumberFormatparsing; server has no bounds —annualIncomeunbounded,emergencyContactPriorityunbounded — client caps 0–9999, propose serverMin/Max). - pickupAuthorization boolean.
- Server 400
VALIDATION_ERRORdetails map to fields (http-exception.filter.ts:104-107).
States:
- saving → CTA spinner, fields disabled, no double-submit.
- 409
DUPLICATE_RESOURCE(create) → inlineAppBanner(error)"A guardian profile already exists for this user." + button "Open existing profile" →/parents/:id(parent.service.ts:32-34). - 404 (edit) →
AppErrorState"Parent not found." (parent.service.ts:77). - success → pop to detail; snackbar "Guardian saved"; audit event
ParentUpdated(event-queue-map.ts:38) orParentCreated→ in-app (event-queue-map.ts:37).
A11y: labels visible + linked; errors announced + focus first invalid
(09 §10); autofill hints organization for company (desktop).
Motion: route push m-base; error slide-in m-fast.
Tablet/desktop: centered card ≤ 560 dp; Enter submits; Tab order = visual.
4. Link Guardian Sheet (from detail / student screen)
Chrome: AppBottomSheet (half → full height on phone when keyboard); DragHandle;
title "Link guardian to student"; scrollable body.
Layout:
Student (if not preselected) AppSearchBar → result rows (admissionNumber + name)
selection replaces field with chosen chip + change
relationship AppDropdown 6 values (enum student-parent-link.schema.ts:7-14)
isPrimaryGuardian AppSwitch "Primary guardian" (default false)
pickupAllowed AppSwitch "Allowed to pick up" (default true — schema:33-34)
financialResponsibility AppSwitch "Financially responsible" (default false)
emergencyPriority AppTextField number (0-99)
CTA "Link guardian" FilledButton fullWidth
Warnings (client-side pre-checks over loaded links):
- Pair already linked →
AppBanner(warning)"Already linked — saving creates a duplicate (server allows it, OQ-2)" + disable CTA or "Link anyway". - Student already has a primary and
isPrimaryGuardiantoggled on →AppBanner(warning)"This student already has a primary guardian. Two primaries will exist." (OQ-5).
States: submitting → CTA spinner; 400 → field errors; 404 student → banner (only if
server ever enforces — today findById result is discarded, OQ-3); success → sheet pops
with link doc → children section refreshes + snackbar "Guardian linked" (lightImpact).
A11y: sheet = dialog semantics; each switch Semantics(toggled:); dropdown role
combobox.
Motion: sheet slide-up m-base; warning banner expand AnimatedSize.
5. Unlink Dialog
Chrome: AppDialog centered card; title "Unlink guardian?"; body:
"Remove 04-Modules/Parents.md:58).
- Primary warning line (error container,
erroricon) whenisPrimaryGuardian: "This child will have no primary guardian after unlinking." (OQ-4). - Actions:
TextButtonCancel /FilledButtondestructive "Unlink". - Behavior: server-first (no optimistic — irreversible);
heavyImpacton confirm; success → row removedm-basefade + snackbar; 404 → treat-as-removed (student-parent-link.service.ts:40).
6. My Children /my/children (forward-looking)
Chrome: shell AppBar "My children"; body = ChildSwitcherBar + selected child
summary.
Composition:
ChildSwitcherBar horizontal chips (avatar + name), selected primaryContainer
ChildSummaryCard name, grade/class/section (student.schema.ts:33-39),
admissionNumber mono
Section entries Attendance / Results / Fees (other modules; student.read)
AppBar action AppAvatar → profile menu → /my/profile
States: loading skeleton; empty AppEmptyState "No linked children"; 404/error →
AppErrorState; offline → cached list + banner.
Blocker note: depends on (planned) my-profile/children endpoint (OQ-1) — the screen
spec is frozen until then; do not build against a client-side workaround.
7. Child Switcher (component)
Spec: ChildSwitcherBar — horizontal SingleChildScrollView of FilterChip-style
avatar chips (40 dp, initials/avatar, name below, primaryContainer selected state);
ChildSwitcherCubit.selectedChildId drives all child-context modules; switch animates
AnimatedSwitcher fade m-fast; a11y chips Semantics(button, toggled); announced
"Switched to
8. My Profile /my/profile (forward-looking)
Same form chrome as §3 (edit mode) with read-only header (identity from users,
relationship per child from links). PATCH /parents/:id on save
(parent.service.ts:74-87). Blocks: no self-id resolution (OQ-1) — frozen spec.
Shared micro-interaction & motion notes (all screens)
- Route transitions
m-base; snackbar success < 3 s, error ≥ 4 s (00-shared/03 A). - Cards
e-1; sheetse-5; dialogse-4; FABe-3,r-full. - Touch targets ≥ 48 dp; rows ≥ 56 dp (
02 §9). - Safe areas respected; landscape lists scroll.
- Reduced motion: fades only, no stagger/shake (
08 §4).