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

13 — State Management (Students Module)

Per-screen Cubits (Flutter/bloc; proposal — 00-shared/06) + repositories for the endpoints in 12_API_Mapping.md. Conventions: one Cubit per screen, LoadState machine (00-shared/06 §3.1), PaginatedListMixin for the roster (00-shared/06 §3.2).


1. Module data model (client)

class Student {
  String id; String userId;
  String admissionNumber; String? rollNumber;
  String academicYearId; String? campusId;
  String gradeId; String sectionId; String classId; String? houseId;
  DateTime? admissionDate; String? admissionType; StudentStatus status;
  bool transportRequired; bool hostelRequired; String? medicalNotes;
  Map<String, dynamic>? metadata;
  // hydrated (joined):
  UserRef? user;      // name/email/phone/avatarUrl from GET /users/:id
  ClassRef? classRef; // label from academics refs (dropdown sources)
}

enum StudentStatus { active, inactive, graduated, transferred, archived }

class Enrollment {
  String id; String studentId; String classId; String academicYearId;
  String? rollNumber; DateTime joinedAt; DateTime? leftAt; EnrollmentStatus status;
}

class StudentDocument {
  String id; String studentId; String fileName; String mimeType;
  int size; String fileId; String? category; String? uploadedBy; DateTime createdAt;
}

class ParentLink {
  String id; String studentId; String parentId; RelationshipType relationship;
  bool isPrimaryGuardian; bool financialResponsibility; bool pickupAllowed;
  int emergencyPriority;
}

class ImportReport {
  String entity; int totalRows; int imported; int failed;
  List<ImportRowError> errors; // {rowNumber, errors[]}
}

Enums map 1:1 to server (student.schema.ts:7-13, class-enrollment.schema.ts:7-11, student-parent-link.schema.ts:7-14). user/classRef hydration is client-side join (GET /users/:id + academics refs) — no server join exists.

2. Cubit map

ScreenCubitEvents → State
RosterStudentsListCubitLoad(page1), LoadMore, Refresh, ChangeFilters(status/class/year), Search(q), Retry{load: LoadState, items[], meta, filters, query, searchMode}
DetailStudentDetailCubitLoad(id), Refresh, `RunAction(Action.graduate
Create wizardCreateStudentCubit`IdentityStep(createUser
Enroll sheetEnrollCubitSubmit(classId, yearId, roll){idle, submitting, success, error}
TransferTransferCubitLoad(current), Submit(form){idle, loading, submitting, blocked(status), success, error}
DocumentsDocumentsCubitLoad, Upload(file, category), RetryUpload{load, docs[], uploading{tile}, error}
HistoryHistoryCubitLoad{load, enrollments[]} (academic-history + active enrollments merged)
Parents tabParentLinksCubitLoad, CreateParent(form), Link(dto), Unlink(id){load, links[], sheetState}
Import wizardImportCubitDownloadTemplate, PickFile(parsed), Upload, Retry{step, file, report?, submitting}

3. Roster state machine (pagination + filters)

stateDiagram-v2
    [*] --> initial
    initial --> loading : Load
    loading --> success(data,meta) : 200
    loading --> error(code) : 4xx/5xx
    success --> loadingMore : LoadMore (hasNext)
    loadingMore --> success (append) | error(tail banner)
    success --> loading : Refresh / ChangeFilters / Search (reset page)
    success --> empty : totalItems == 0 | filters no match
    error --> loading : Retry
  • PaginatedListMixin contract: page, limit(20), hasNext, items, loadFirst/loadMore/pullToRefresh (00-shared/06 §3.2).
  • Server q/sort ignored (OQ-2) → filters/search run client-side over accumulated pages; when filter set, LoadMore keeps paging server-side and applies filter locally. searchMode flag toggles helper text (honesty rule 09 §1).
  • Cache: last-good list per key sl:{tenant}:students:list:{page} (Hive/prefs, TTL 5 min (proposed)) — served instantly offline, refreshed in background.

4. Detail tabs lazy-load

flowchart TD
    A[StudentDetailCubit Load] --> B[profile: GET /students/:id]
    B --> C{tab activated}
    C -->|Documents| D[GET /students/:id/documents]
    C -->|History| E[GET /students/:id/academic-history<br/>+ GET /students/:id/enrollments]
    C -->|Attendance/Fees/Results| F[other-module repos — forward-looking]
    D & E --> G[kept alive; Refresh re-fetches]
  • Each tab owns its LoadState; failures isolated (tab shows compact AppErrorState, screen stays).
  • After RunAction success → Refresh() profile + badge; history refresh on transfer/enroll; documents refresh after upload.

5. Create wizard state

class CreateStudentState {
  int step;                 // 0..3 (identity, academics, extras, review)
  CreateUserDraft user;     // firstName, middleName?, lastName, email, phone?
  bool reuseExistingUser; String? userId;
  ClassSelection selection; // academicYearId, gradeId, sectionId, classId
  ExtrasDraft extras;       // admissionDate?, admissionType?, campusId?, houseId?,
                            // transportRequired, hostelRequired, medicalNotes?, rollNumber?
  bool submitting; String? createdId; WizardError? error; // field-targeted
}
  • Submit sequence (exact): if new user → usersRepo.create → on 409 switch to reuseExistingUser prompt; then studentsRepo.createcreatedId → navigate /students/:id.
  • No draft persistence across app kills (YAGNI — 09 §2).
  • 400 field errors mapped via details[].field; 409 admission → field error on admission number.

6. Import wizard state (progress contract)

class ImportState {
  int step;                      // template | file | uploading | report
  ParsedCsv? preview;            // headers + first rows + column warnings
  bool submitting;
  ImportReport? report;
  ImportPhase phase;             // idle | uploading(simulated) | done
}
  • Server import is synchronous (loop in request, bulk-import.service.ts:45-63) — no job id, no progress API (OQ-12). UI: indeterminate progress + elapsed rows readout (client estimate totalRows), disable back during upload.
  • Report render from ImportReport; rowNumber-grouped errors; "Done" resets list cache (invalidate roster).

7. Documents upload state

UploadingDoc {fileName, size, progress(0..1), phase: queued|uploading|done|error} in DocumentsCubit; multipart via dio FormData; retry re-posts buffered bytes; no chunked resume (00-shared/12 B7).

8. Events & actions map (UI → Cubit → Repository)

UI eventCubit methodRepository call
Roster open / refreshload() / refresh()studentsRepo.list(page, limit)
Infinite scrollloadMore()studentsRepo.list(page+1, limit)
Search / filterssearch(q) / setFilters(f)client-side (plus server page fetch)
Row → Viewselect(id)detail load(id)
Create submitsubmit()usersRepo.createstudentsRepo.create
Enrollsubmit(form)studentsRepo.enroll(id, dto)
Transfersubmit(form)studentsRepo.transfer(id, dto)
Graduate/Archive/RestorerunAction(a)`studentsRepo.graduate
DeleterunAction(delete)studentsRepo.delete(id)
Documents load/uploadload() / upload(file, cat)documentsRepo.list(id) / documentsRepo.upload(id, file, cat)
History loadload()studentsRepo.academicHistory(id) + enrollments(id)
Parents loadload()parentsRepo.linksByStudent(id)
Create parentcreateParent(draft)usersRepo.createparentsRepo.create
Linklink(dto)parentsRepo.link(studentId, dto)
Unlinkunlink(linkId)parentsRepo.unlink(linkId)
Import templatedownloadTemplate()bulkRepo.exportCsv('students') → file save/share
Import uploadupload(file)bulkRepo.importCsv('students', file)

9. Realtime & cross-screen

  • No student WS topics (00-shared/07 §8); roster freshness via refresh + focus re-fetch; roster.changed (forward-looking).
  • StudentCreated in-app notification tap → deep link /students/:id (proposed).
  • Academic-year switcher (00-shared/06 §4 selectors) affects class cascade defaults in create/enroll/transfer forms.

10. Error states per action

ActionErrorState →
create409 admission / emailfield error; reuse-user prompt
create400field map
transfer409 statusblocked(status) → read-only banner
lifecycle409 same-statesnackbar + refresh
unlink/delete404treat as removed
uploadnetwork losstile error + retry
import400 parsewizard banner
any429countdown
any5xxgeneric + requestId

11. Testing hooks (00-shared/06 §6)

  • Pure-Dart cubits with mocked repositories; roster state-machine matrix (initial→loading→success→empty→error→loadingMore); wizard sequence test (create user 409 → reuse path); import report rendering (mixed success).
  • Widget tests per screen: 3 states (loading/error/success+empty) per 00-shared/10 §9.