15 — Flutter Implementation Guide (Library Module)
- 1. Module layout (feature-first)
- 2. Models (parse raw docs; never derive server truth)
- 3. Repository (one class, typed errors)
- 4. Cubits
- 5. Caching
- 6. Error UX mapping
- 7. Permissions-aware UI (RBAC planned — OQ-1)
- 8. Date & fine handling
- 9. Testing
- 10. Known ceilings (ponytail notes)
Concrete build guide for the library client on top of the app architecture in 00-shared/11. Follows the shared repo/conventions; module specifics only.
1. Module layout (feature-first)
lib/
features/library/
data/
models/book.dart # BookDoc → Book
models/borrow_record.dart # BorrowRecordDoc → BorrowRecord
library_repository.dart # dio → endpoints (12_API_Mapping.md)
library_local_cache.dart # catalog/borrows TTL caches
domain/
fine_calculator.dart # ceil((now-due)/day) * 5 (mirror library.service.ts:211-217)
presentation/
catalog/ (catalog_cubit.dart, catalog_screen.dart, book_row.dart)
book_detail/ (book_detail_cubit.dart, book_detail_screen.dart)
book_form/ (book_form_cubit.dart, book_form_screen.dart)
issue/ (issue_cubit.dart, issue_sheet.dart)
my_borrows/ (borrows_cubit.dart, my_borrows_screen.dart, borrow_record_card.dart)
return_/ (return_cubit.dart, return_sheet.dart)
overdue/ (overdue_cubit.dart, overdue_screen.dart)
widgets/ (availability_badge.dart, fine_chip.dart, overdue_tag.dart,
copy_stepper.dart, fine_summary_panel.dart)
2. Models (parse raw docs; never derive server truth)
enum BookStatus { available, borrowed, damaged, lost } // book.schema.ts:7-12
enum BorrowStatus { active, returned, overdue, lost } // borrow-record.schema.ts:7-12
enum FineStatus { pending, paid, waived } // :14-18
class Book { final String id, title, author, isbn; final String? publisher,
category, edition, shelfLocation, description; final int totalCopies,
availableCopies; final BookStatus status; }
bool get isAvailable => availableCopies > 0;
class BorrowRecord { final String id, bookId, studentId; final String? issuedBy;
final DateTime borrowedAt, dueDate; final DateTime? returnedAt;
final BorrowStatus status; final double fineAmount; final FineStatus? fineStatus;
final String? notes; final Book? book; } // bookId populated: borrow-record.repository.ts:31,42
bool get isOverdue => status == BorrowStatus.active && DateTime.now().isAfter(dueDate);
- Keep enums as strings matching server values — no renaming.
fineAmountisdouble/num(plain units; no currency —library.service.ts:25).
3. Repository (one class, typed errors)
class LibraryRepository {
Future<Paged<Book>> searchBooks({int page = 1, int limit = 20, String? q});
Future<Book> getBook(String id);
Future<Book> createBook(CreateBookDto dto);
Future<Book> updateBook(String id, UpdateBookDto dto);
Future<void> deleteBook(String id);
Future<BorrowRecord> issueBook({required String bookId, required String studentId,
required DateTime dueDate, String? notes});
Future<BorrowRecord> returnBook({required String borrowRecordId,
double? fineAmount, String? notes});
Future<List<BorrowRecord>> borrowHistory(String studentId); // controller.ts:73-77
Future<List<BorrowRecord>> activeBorrows(String studentId); // controller.ts:79-83
Future<BorrowRecord> payFine(String borrowRecordId); // controller.ts:85-89
}
- Unwrap
data/metafrom the envelope (response-envelope.interceptor.ts:44-52); throwApiException(code, message, details, requestId)onsuccess:false. - ISO dates: parse
dueDate/borrowedAt/returnedAtwith UTC-safe parsing; senddueDate.toIso8601String()(@IsDateString,issue-book.dto.ts:14-15). - No request body ever includes
tenantId(JWT-scoped).
4. Cubits
Implement the seven Cubits from 13_State_Management.md:
CatalogCubit, BookDetailCubit, BookFormCubit, IssueCubit, BorrowsCubit,
ReturnCubit, OverdueCubit (+ inline FinePayCubit). Rules:
- No optimistic writes — every mutation emits server-confirmed docs.
- 409 = refresh, never retry;
noCopies/capReachedare named states. - Double-tap guard — submit states are final until the response returns.
- Overdue/fine-preview derived via
FineCalculator(unit-testable pure function). - Cross-cubit refresh table (
13 §10): issue → refresh detail + borrows; return → detail + borrows + overdue; pay → borrows.
5. Caching
| Data | Key | TTL | Notes |
|---|---|---|---|
| Catalog page | lib:catalog:{tenant}:{q}:{page} | 5 min | stale-while-revalidate; pull-to-refresh bypasses |
| Book detail | lib:book:{id} | 1 min | focus re-fetch |
| Active borrows | lib:active:{studentId} | 1 min | focus re-fetch |
| History | lib:history:{studentId} | 5 min | focus re-fetch |
| Writes | — | never cached |
6. Error UX mapping
| Code | Handling |
|---|---|
400 VALIDATION_ERROR | map details[].message → field errors (forms) |
409 DUPLICATE_RESOURCE | context banners: ISBN dup (inline), no copies, 5-cap, not-active |
| 404 | empty states / close form + snackbar |
| 401 | refresh token once → retry → sessionExpired |
| 429 | countdown banner, no retry |
| 5xx / network | generic + requestId + retry action |
7. Permissions-aware UI (RBAC planned — OQ-1)
Gate from the user's role permissions (matrix in 02 §6):
books.read (module visible), books.create (FAB), books.update (edit),
books.delete (menu), books.issue (issue CTA), books.return (return button),
fines.pay (pay action). Until the server enforces, the client hides what the role
lacks; when guards land, keep the same gates.
8. Date & fine handling
- Due dates: native date picker; client min = tomorrow; default +14 days.
FineCalculator.daysOverdue(dueDate) = ceil((now − dueDate).inDays fractional)— must mirrorMath.ceil(diffMs / dayMs)(library.service.ts:215).- Fine preview is always labeled estimate; after return, render the server's
fineAmountverbatim. - Timezone: compare dates in the tenant's local day boundary
(proposed); today use UTC instants to stay consistent with the server'snew Date()comparisons.
9. Testing
- Unit:
FineCalculator(due-today → 0; +1 ms → 5; +3.5 d → 20), model parsing, enum mapping,BookRowbadge states. - Cubit: issue 409 paths (
noCopies,capReached), returnnotActive, fine pay gating, catalog filter logic. - Widget: sheet non-dismissible during submit; FAB hidden without permission.
- Integration: repository against the API with mocked envelope + error bodies.
- E2E (needs Mongo + Redis): the scenarios in
14_QA_Checklist.mdQA-1/2/4/6.
10. Known ceilings (ponytail notes)
- Overdue screen is derived data; replace with the server endpoint when the scan
ships
(planned)(IMPLEMENTATION_PLAN.md:228). - No QR/barcode scan yet —
(forward-looking); keep the issue flow keyed onbookIdso a scanner can later inject the same id. - Analytics
(proposed)— hook names from05 §Analyticswhen the SDK lands.