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 (Library Module)

Full functional spec per screen: layout, states, copy, keyboard, a11y, motion, data bindings and error mapping. Wire contracts in 12_API_Mapping.md; components in 07_Component_Library.md; Cubit behavior in 13_State_Management.md.


S1. Catalog List + Search (/library)

Layout (portrait phone)

┌───────────────────────────────────────────┐
│ AppBar  "Library"            [avatar]     │
│ ┌─ SearchBar  q=… ─────────────────────┐  │
│ [chips] All · Category · Status · Avail │  │   (client-side filters, OQ-5)
│ ─────────────────────────────────────── │  │
│ [list] BookRow                           │  │
│   Cover-thumb?  Title                    │  │
│                 Author · ISBN            │  │
│                 [avail badge] [status]   │  │
│ [list] … (infinite scroll)               │  │
│ ─────────────────────────────────────── │  │
│                          [FAB +]         │  │
└───────────────────────────────────────────┘

Behavior

AspectSpec
SearchAppSearchBar, debounce 300 ms → q; server regex title/author/ISBN (book.repository.ts:26-30); clearing returns full list (library.service.ts:57-74)
RowBookRow: title (2 lines max), author · ISBN, right-aligned availabilityBadge ($available/$totalCopies) + BookStatus badge
Paginationinfinite scroll, page/limit=20; meta.hasNext from buildPaginationMeta (library.service.ts:75) stops the loader
Filterschips filter in memory (server supports no status/category filter — OQ-5); combos: category, status, availability (availableCopies > 0)
Empty"No books found" + clear-search action; distinguish "no search results" vs "catalog empty"
Erroroffline → AppOfflineBanner + cached list; 429 → countdown banner; 5xx → error state + retry (requestId in snackbar)
Permissionno books.read → empty state with lock copy (RBAC (planned), OQ-1); no books.create → FAB hidden
Refreshpull-to-refresh → re-fetch page 1; focus-return also refreshes (issue/return may have changed availability)
a11ysearch focused on screen open (large catalogs); results announced via live region; row semantics = button
Motionlist uses standard AppListTile transitions; search debounce uses m-fast progress shimmer (00-shared/08)
Analyticslibrary.catalog.search, library.catalog.open_book (proposed)

States table

StateUI
idlecached list or empty
loadingAppSkeleton rows (first load); shimmer under search (subsequent)
emptyAppEmptyState "No books found"
errorerror panel + retry; offline banner when no cache
disabledchips disabled while loading filters
permissionlock empty-state (RBAC planned)

S2. Book Detail (/library/books/:id)

Layout

┌───────────────────────────────────────────┐
│ ←  Detail                        [⋯ menu] │
│ ┌───────────────────────────────────────┐ │
│ │ [cover?]  Title (headline)            │ │
│ │ Author — ISBN                         │ │
│ │ [status badge]  [avail badge 3/5]     │ │
│ └───────────────────────────────────────┘ │
│ Metadata: Publisher / Category / Edition  │
│           Shelf / Added (createdAt)       │
│ Description (scrollable)                  │
│ ────────────────────────────────────────  │
│ [Issue book]   (filled, enabled iff avail)│
│ [Edit]  (outlined, books.update)          │
└───────────────────────────────────────────┘

Behavior

AspectSpec
LoadGET /books/:id (library.controller.ts:43-47); 404 → AppEmptyState "Book not found" + back (library.service.ts:80)
AvailabilityavailableCopies > 0 → issue enabled; 0 → disabled + copy "All copies are on loan" (issue is a server 409 too, library.service.ts:109-111)
Status copydamaged/lost (book.schema.ts:7-12) → issue disabled + warning badge (no endpoint transitions to these — OQ-4)
EditPATCH flow (S4); after save, detail re-fetches
Deletemenu → AppDialog confirm → DELETE /books/:id (library.controller.ts:55-59); 409 "Cannot delete book with active borrow records." (library.service.ts:99-101) → dialog explains active loans; 200 → snackbar + pop to catalog (soft delete, library.service.ts:103)
Edit copiescopy delta adjusts availableCopies (floored at 0) (library.service.ts:87-90) — surface "available will change by same amount" note
Active loansnot exposed by any endpoint — skip section (planned)
a11ystatus badges read as full sentences ("3 of 5 copies available"); actions grouped
Motionhero transition from list row (cover/title) m-base (00-shared/08)
Analyticslibrary.book.open, library.book.issue_start, library.book.delete (proposed)

S3. Book Form — Create / Edit (/library/books/new, /library/books/:id/edit)

Layout

┌───────────────────────────────────────────┐
│ ←  New book / Edit book        [Save]     │
│ Title*       ──────────────               │
│ Author*      ──────────────               │
│ ISBN*        ──────────────               │
│ Publisher    ──────────────               │
│ Category     ──────────────               │
│ Edition      ──────────────               │
│ Copies [− 3 +]  (create only*: total)     │
│ Shelf        ──────────────               │
│ Description  ──────────────               │
│   (multiline, optional)                   │
└───────────────────────────────────────────┘

Behavior

AspectSpec
FieldsCreateBookDto (create-book.dto.ts:5-46): title/author/isbn required strings; publisher/category/edition/shelfLocation/description optional; totalCopies optional number @Min(1) default 1 (:32-36); edit uses UpdateBookDto = PartialType (update-book.dto.ts:4) — all optional, copy-count editable
Copies semanticscreate: availableCopies = totalCopies (library.service.ts:41-46); edit: server shifts availableCopies by the delta, floor 0 (library.service.ts:87-90) — show the projected availability inline
Validationclient mirrors server: required title/author/isbn; ISBN numeric-ish; copies ≥ 1; 400 maps details[].message → fields
Conflict409 duplicate ISBN (library.service.ts:37-39) → inline under ISBN: "A book with this ISBN already exists" + link to the existing book if client has it in cache
Savecreate POST /books → 201 → push detail; edit PATCH /books/:id → 200 → pop with updated doc; no optimistic writes
Draftunsaved-changes guard on back; draft persist (proposed)
a11yevery field labelFor-linked; first invalid field focused on 400
Analyticslibrary.book.create, library.book.update (proposed)

S4. Issue Form (/library/books/:id/issue — bottom sheet)

Layout

┌───────────────────────────────────────────┐
│ Issue: "Book Title"          [Confirm]    │
│ Student*  [picker ▾]   (search + select)  │
│ Due date* [date ▾]    (default +14 days)  │
│ Notes     ──────────────                  │
│ ────────────────────────────────────────  │
│ Availability: 3 of 5 copies available     │
└───────────────────────────────────────────┘

Behavior

AspectSpec
Student pickerfrom Students module profile state; free-text search, one selection; disabled while no student selected
Due dateAppDateField; must be future (client blocks past dates; server has no min — server accepts any date issue-book.dto.ts:14-15, so client must enforce)
SubmitPOST /books/issue {bookId, studentId, dueDate, notes?} (library.controller.ts:61-65)
201record status=ACTIVE, borrowedAt=now (library.service.ts:132-139) → close sheet, snackbar "Issued to {student}", refresh detail + my-borrows
409 no copiesbanner "No copies available for borrowing." + re-fetch book (library.service.ts:109-111)
409 max 5banner "Student already has maximum number of borrowed books." + show their active count from cached my-borrows (library.service.ts:116-120)
404book deleted meanwhile → close sheet, pop detail with snackbar
BusyConfirm shows spinner; sheet not dismissible during submit (prevents double-tap — see QA-2)
a11ysheet is modal, focus first field; server messages read via live region
Analyticslibrary.issue.submit, library.issue.conflict(kind) (proposed)

S5. Borrowed-by-Me — Active / History (/library/my-borrows)

Layout

┌───────────────────────────────────────────┐
│ ←  My borrows                     [overdue▾]│
│ [Active (2)]  [History (11)]               │
│ ────────────────────────────────────────  │
│ [Card] Book Title                          │
│   borrowed 12 Jul · due 26 Jul  ← 4d [!!]  │
│   fine 20 (pending)            [Return]    │
│ [Card] …                                  │
└───────────────────────────────────────────┘

Behavior

AspectSpec
TabsActive = GET /books/borrows/:studentId/active (library.controller.ts:79-83); History = GET /books/borrows/:studentId (library.controller.ts:73-77) — includes returned + any active; not paginated (library.service.ts:195-201); client shows first N with "load more" (proposed)
Populaterepo populates bookId (borrow-record.repository.ts:31,42) → title/author render; studentId is not populated — student identity comes from the profile context (OQ-9)
Due dateprominent on active cards; overdueTag when active && now > dueDate (derived, OQ-2); fine preview ceil(days)×5 shown but marked "estimate"
Fine chipfineStatus == pending → amber chip with amount + "Pay" (fines.pay); paid → green; waived → grey (enum borrow-record.schema.ts:14-18)
Returnstaff sees [Return] on active cards → Return Form (S6); student/parent do not
History rowsreturnedAt + final fineAmount; no actions
Emptyactive: "Nothing on loan right now"; history: "No past borrows"
Refreshpull-to-refresh re-fetches both tabs
a11yoverdue announced ("overdue by 4 days"); fine chips full-sentence semantics
Analyticslibrary.myborrows.open (proposed)

S6. Return Form (/library/my-borrows/return/:recordId — bottom sheet)

Layout

┌───────────────────────────────────────────┐
│ Return: "Book Title"           [Confirm]  │
│ Borrower: Student Name                    │
│ Due: 26 Jul · Returned today              │
│ Days overdue: 4                            │
│ Fine (auto)  [20]   (editable, staff)     │
│ Notes ──────────────                       │
│ ────────────────────────────────────────  │
│ Fine preview = 4 days × 5/day (auto)       │
└───────────────────────────────────────────┘

Behavior

AspectSpec
Prefilldays overdue = ceil((now − dueDate)/day); fine preview = days × 5client mirrors server rule library.service.ts:211-217; server recomputes anyway (:159)
OverridefineAmount? editable @Min(0) (return-book.dto.ts:9-13) — server prefers dto.fineAmount ?? calculateFine(dueDate) (library.service.ts:159); keep override only for staff (hide for student/parent — they can't return anyway)
SubmitPOST /books/return {borrowRecordId, fineAmount?, notes?} (library.controller.ts:67-71)
200record status=RETURNED, returnedAt=now, fineAmount, fineStatus = pending if fine > 0 else unset (library.service.ts:161-169); book availableCopies+1, status available (library.service.ts:172-178)
409"Book was not actively borrowed." (library.service.ts:155-157) → refresh record, banner "Already returned"
Fine payif fineAmount > 0 → inline secondary action "Pay fine" → confirm dialog → POST /books/fines/:id/pay (library.controller.ts:85-89) → chip → paid (library.service.ts:203-209)
Busyspinner on Confirm; no dismiss during submit
a11yfine amount + status live-regioned after submit
Analyticslibrary.return.submit, library.fine.pay (proposed)

S7. Overdue View (/library/overdue)

Layout

┌───────────────────────────────────────────┐
│ ←  Overdue                    [refresh]   │
│ ⚠ Derived from client data — server       │
│   overdue scan planned                    │
│ [Card] Student · Book Title               │
│   due 12 Jul · 15 days late · fine 75     │
│                            [Return]       │
└───────────────────────────────────────────┘

Behavior

AspectSpec
Dataderived by filtering active borrows where now > dueDate (per-student active lists; no list-all endpoint — OQ-2). Server findOverdue() exists but is unused (borrow-record.repository.ts:34-43); a dedicated endpoint + scheduled scan is (planned) (IMPLEMENTATION_PLAN.md:228)
Honestypersistent info banner: "Derived from client data — server overdue scan planned"
Rowsstudent (from profile state), book title, dueDate, days late, fine preview ceil(days)×5, [Return] shortcut → S6
Empty"Nothing overdue"
Refreshre-fetches the underlying active lists
Permissionstaff only (books.issue/books.return); hidden otherwise
Analyticslibrary.overdue.open (proposed)