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

Detailed, build-ready specs for the announcement surface. Biggest file in this package. Data contracts are verbatim from announcement.schema.ts, create-announcement.dto.ts, announcement.service.ts, announcements.controller.ts; gaps are flagged (planned) / (forward-looking) and must not be invented as shipped behavior.


1. Announcement Feed (/announcements)

1.1 Purpose & entry

Recipient-first consumption surface. Entry: dashboard Communication hub, cold start, push deep link (forward-looking).

1.2 Data contract

GET /announcementsAnnouncementDocument[] (no pagination in backend; announcement.service.ts:56-60 returns full array sorted createdAt: -1).

Item shape (schema announcement.schema.ts:31-53 + BaseSchema):

{
  "_id": "…", "tenantId": "…", "title": "Diwali holiday",
  "body": "School closed Fri.",
  "audience": { "type": "grade", "value": "Grade 10" },
  "targetUserIds": ["…", "…"],
  "published": true, "publishedAt": "2026-…",
  "readBy": [{ "userId": "…", "readAt": "2026-…" }],
  "attachments": ["https://…/notice.pdf"],
  "createdBy": "…", "createdAt": "…", "updatedAt": "…"
}

1.3 Layout (mobile)

┌────────────────────────────┐
│ AppBar "Announcements"  [FAB+]│
│ TabBar: For me | All        │
│ ┌──────────────────────────┐│
│ │ AnnouncementCard         ││
│ │  [•unread] Diwali holiday ││
│ │  School closed Fri.…     ││
│ │  [grade] [Published] 2d  ││
│ └──────────────────────────┘│
│ … (list)                    │
└────────────────────────────┘

1.4 Behavior

  • "For me" tab (client-side): include item if audience.type === 'all' OR my userId ∈ targetUserIds. Backend never filters per user (announcement.service.ts:56-60) → client filter (forward-looking).
  • Unread indicator: readBy lacks my userId → unread dot (announcement.schema.ts:51-52).
  • Audience chip: derived from audience.type + value (e.g. role: teacher, section: 10-A) (announcement.schema.ts:7-21).
  • Status badge: draft (published: false) vs published (:45-49); draft rows only meaningful on "All"/"Mine".
  • Pull-to-refresh; infinite scroll not supported (no pagination API — gap, (planned) when tenant grows).
  • Tablet: list + detail two-pane (master-detail per 00-shared/05).

1.5 States

StateRender
loadingAppSkeleton rows ×6
successlist
emptyAppEmptyState "No announcements yet" + compose CTA (authors)
error offlineAppOfflineBanner + cached list (forward-looking); retry
auth expired401 → login redirect

1.6 Events

comm.announcements.open|refresh|tab_switch (proposed).


2. Announcement Detail (/announcements/:id)

2.1 Data contract — GAP

No GET /announcements/:id route exists (announcements.controller.ts:19-52). The client must carry the item object from the feed/list, or the row must be a summary-only sheet. A dedicated detail fetch is (planned).

2.2 Layout (sheet on mobile / pane on tablet)

┌────────────────────────────┐
│ [badge: published] [chip]   │
│ Title (headline)            │
│ To: Grade 10 · 120 people   │   ← audience.value + targetUserIds.length
│ Published 3 Aug 2026        │
│ ─────────────────────────── │
│ Body paragraph text         │
│ ─────────────────────────── │
│ [attachment] notice.pdf ↗  │
│ ─────────────────────────── │
│ [ Mark as read ] (filled)   │
└────────────────────────────┘

2.3 Behavior

  • Mark read: POST /announcements/:id/read$addToSet receipt, idempotent (announcement.repository.ts:20-33); NotFound → snackbar (announcement.service.ts:100).
  • Auto-mark on open is a product decision (proposed); explicit button also shown for drafts-only confusion.
  • Audience line: audience.value for role/grade/section/custom; "Everyone" for all; count = targetUserIds.length (0 for allannouncement.service.ts:144-146; show "Everyone" without a count in that case to avoid "0 people" confusion).
  • Drafts: authors see draft detail (no read button; "Publish" CTA instead).

2.4 States

loading (if fetched (planned)), success, not-found (pop + snackbar), offline.

2.5 Events

comm.announcement.detail.{open,mark_read,attachment_open} (proposed).


3. Compose Announcement (/announcements/compose)

3.1 Layout (scrollable form)

┌────────────────────────────┐
│ AppBar "New announcement"   │
│ [Save draft]  [Publish]    │  ← Publish = save + publish
│ Title*                      │  AppTextField, min 1 char
│ Body*                       │  multiline, min 1 char
│ Audience*                   │  [type picker] [value field/selector]
│   type: all | role | grade | section | custom
│   value: string or string[] (see 3.3)
│ ⚠ 0 recipients warning (planned)
│ Attachments (planned)       │  chips + picker
│ Priority / Expiry (planned) │  toggle + date picker — NO schema fields yet
└────────────────────────────┘

3.2 Data contract (verbatim create-announcement.dto.ts:29-49)

{
  "title": "string (min 1)",
  "body": "string (min 1)",
  "audience": {
    "type": "all|role|grade|section|custom",
    "value": "string | string[]"       // required per type; absent for "all"
  },
  "attachments": ["string"]            // optional
}

3.3 Audience picker rules

Type (announcement.schema.ts:7-13)Value inputResolves to
allnone (value omitted)[] — broadcast marker only (announcement.service.ts:144-146)
roleenum string — e.g. teacheractive org members with role
(announcement.service.ts:114-119)
gradegrade name stringstudents of grade (:120-125)
sectionsection name stringstudents of section (:126-135)
customuserId chipsthe listed users (:136-143)
  • Grade/section inputs are names, not ids (findOne({ name: audience.value }), announcement.service.ts:121,128) — present a picker fed by the academics module, not free text, to avoid silent empty audiences.
  • Zero-recipient warning (planned): after publish attempt backend resolves silently to [] (announcement.service.ts:122,129) — client should preview count (planned).

3.4 Save vs Publish

  • Save draft: POST /announcementspublished: false + createdBy from token context (announcement.service.ts:31-37); emits AnnouncementCreated (:39-51).
  • Publish: same POST then POST /announcements/:id/publish → resolves audience to targetUserIds, sets published: true, publishedAt (:68-93); emits AnnouncementPublished (:80-91). Idempotent re-publish returns early (:68-70).
  • Editing an existing draft not supported (no update route) — gap; re-create. (planned).

3.5 States & validation

CheckRuleSource
titlerequired, min 1 char, trimmedcreate-announcement.dto.ts:30-33; announcement.schema.ts:33-34
bodyrequired, min 1 char, trimmedcreate-announcement.dto.ts:35-38; announcement.schema.ts:36-37
audience.typerequired enumcreate-announcement.dto.ts:14-16; announcement.schema.ts:16-17
audience.valueoptional string | string[], strings onlycreate-announcement.dto.ts:24-26
attachmentsoptional string[]create-announcement.dto.ts:45-49

Publish with empty audience.value for role/grade/section → block + inline error (planned); today backend accepts and resolves [] (announcement.service.ts:120-143).


4. Read Receipts (/announcements/:id/reads)

4.1 Data contract

GET /announcements/:id/readsAnnouncementDocument['readBy'] — array of { userId, readAt } (announcement.service.ts:104-107, announcement.schema.ts:23-29). Does not include targetUserIds — client needs the list payload for the unread complement.

4.2 Layout

┌────────────────────────────┐
│ "Diwali holiday"           │
│ 72 of 120 read · 48 unread │  ← readBy.length / targetUserIds.length
│ [Read] [Unread]  (segmented)│
│ • Aisha Idris    3 Aug 9:04 │
│ • Samuel Eze    3 Aug 9:12 │
└────────────────────────────┘

4.3 Behavior

  • Unread set = targetUserIds − readBy.userId (client-side; user display names from user lookup (forward-looking) — no join in API).
  • Refresh on pull; auto-refresh 30 s while open (proposed).
  • Export / remind laggards (planned).
  • For all-type announcements targetUserIds is [] → counts meaningless; hide numbers, show receipts list only.

5. Threads List (/threads) — secondary

GET /threads (threads.controller.ts); rows: partner avatar, last message preview, unread badge. Empty state + FAB. Tablet two-pane.

6. Thread Detail (/threads/:id) — secondary

GET /threads/:id + POST /messages + PATCH /threads/:id/read (docs/IMPLEMENTATION_PLAN.md:107-112). Bubbles, input bar, optimistic send, offline outbox (forward-looking). WebSocket realtime (planned) (IMPLEMENTATION_PLAN.md:119). Full spec deferred to a dedicated messaging package.


7. Cross-screen invariants

  1. published: false ⇒ never in recipient "For me"; authors see "Draft" badge.
  2. Unread state = myUserId ∉ readBy[].userId — always derived, never cached server-side.
  3. AudienceType.ALLtargetUserIds.length === 0 — treat as "everyone", never as "0".
  4. Every write that touches a doc emits an event (AnnouncementCreated / AnnouncementPublished, announcement.service.ts:39-51,80-91) — client may subscribe via WS (planned) for live feed updates.
  5. All reads tenant-scoped by BaseRepository (announcement.repository.ts:12-18) — cross-tenant leakage impossible at API level; client must not assume the same for display names.

8. Open questions

  • OQ-1: Should GET /announcements paginate before feed ships? (backend today returns unbounded array — announcement.service.ts:56-60)
  • OQ-2: Should opening detail auto-mark read, or require explicit tap?
  • OQ-3: Are grade/section pickers fed by academics API (GET /academics/…) — needs module contract check.
  • OQ-4: When communication.* RBAC perms arrive, which roles may compose/publish? (blueprint: notification.* analog — studylyon-blueprint/04-Modules/Notifications.md:65-71)