Extends 00-shared/06 . Bloc/Cubit, one cubit per screen; repository layer only
place touching HTTP; typed ApiException(code, status, fieldDetails).
Cubit Screen State Notes
TeachersListCubitS1 TeachersListStatepaginated mixin + filters
TeacherDetailCubitS2–S4 TeacherDetailStatetabs, year selection
TeacherFormCubitS5/S6 TeacherFormStatecreate/update + dirty tracking
AssignmentEditorCubitS7 AssignmentEditorStatepickers + duplicate guard
AssignmentsTabCubitS3 AssignmentsTabStateyear-keyed matrix
ScheduleTabCubitS4 ScheduleTabStateweek grid
MyTeachingCubitS9/S10 (planned) MyTeachingStatecomposed self-view
// S1
class TeachersListState {
LoadState load; // Initial | Loading | Success | Error
List<Teacher> items; // merged pages
int page; bool hasNext;
List<EmploymentStatus> statusFilter;
String? departmentId, designationId, query; // client-side filters (OQ-3)
}
// S2–S4
class TeacherDetailState {
LoadState load;
Teacher? teacher;
int tabIndex; // 0 profile, 1 assignments, 2 schedule
AcademicYear currentYear; // default isCurrent
List<SubjectAssignment>? assignments;
List<TimetableEntry>? timetable;
int yearRequestToken; // drop stale year-switch responses
}
// S5/S6
class TeacherFormState {
LoadState submit; // idle | submitting | success | error
TeacherFormData data; // field DTO mirror (F1/F2)
Map<String, String> fieldErrors; // from 400 details
ConflictType? conflict; // user | employeeNumber (409)
bool dirty;
}
// S7
class AssignmentEditorState {
LoadState submit;
String? subjectId, classId, academicYearId;
bool duplicateDetected; // client-side (server gap, OQ-2)
}
class TeacherRepository {
Future<Page<Teacher>> list({page, limit}); // GET /teachers
Future<Teacher> byId(String id); // GET /teachers/:id
Future<Teacher> create(TeacherDraft dto); // POST /teachers
Future<Teacher> update(String id, TeacherPatch dto);// PATCH /teachers/:id
Future<void> remove(String id); // DELETE /teachers/:id
Future<List<SubjectAssignment>> assignmentsByTeacher(String teacherId, String yearId);
Future<SubjectAssignment> createAssignment(CreateSubjectAssignmentDto dto);
Future<void> removeAssignment(String id);
}
Cache key TTL Policy
sl:{tenant}:teachers:page:{n}5 min stale-while-revalidate (06 §3.3)
sl:{tenant}:teachers:refs:{depts|designations|subjects|classes|years}24 h reference data
sl:{tenant}:teachers:assignments:{teacherId}:{year}5 min volatile matrix
detail teachers:{id} none server-fresh on every visit
RefreshIndicator always bypasses cache (06 §3.3).
LoadTeachers, LoadMore, RefreshTeachers, ChangeStatusFilter(dept, designation),
Search(q), ClearFilters
LoadTeacher(id), ChangeTab(i), ChangeYear(yearId), RefreshTab
SubmitCreate(draft), SubmitUpdate(patch), Discard
SelectSubject/Class/Year, AddAssignment, RemoveAssignment(id)
Naming per 00-shared/06 §4.
notification.new (WS, 00-shared/07 §8) for own tenant → teachers list marks
refresh-needed badge (planned); no live row mutation (soft-delete is 404-driven).
Dashboard teachers.total refreshes on TeacherCreated/Deleted events when
dashboard visible (via DashboardCubit refresh — dashboard.service.ts:32).
Action Optimistic?
List filters, search, year switch yes (local state)
Assignment add/remove no — server-confirmed (no server dedup; confirm before UI change)
Teacher create/update no
Deactivate no (irreversible)
Pull-to-refresh cache-bypass reload
409 conflict → form-level state (ConflictType), never clears fields.
400 → fieldErrors from error.details (00-shared/07 §3).
404 → detail/list treat as removed: navigate back with snackbar.
401 → AuthCubit refresh; fail → session expiry.
5xx → AppErrorState with requestId; retry re-emits Load event.
TeachersListCubit pure-Dart unit tests (mock TeacherRepository): pagination
merge, filter application, duplicate detection, conflict mapping.
Widget tests: S1 3-state (loading/error/empty), S5 409-banner, S7 duplicate-block.
AppStateObserver transition logs dev-only (06 §6).