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

Detailed specifications for every screen in 05_Screen_Inventory.md. Layout is mobile-first; tablet/desktop adaptations noted per screen. All states, copy, and API sources derive from src/modules/crm/ (file:line cited). Shared atoms from 00-shared/02-04 (Design Tokens, Component Library, Design System Mapping).


S1 — Leads List (/crm/leads)

Purpose & entry

Primary working screen for counselors. Entry: CRM tab, back from S2. Exit: S2 (row), S3 (FAB), filtered list.

Layout (mobile)

[AppBar: "Leads" | search icon (planned) | overflow]
[Status filter bar: All | New | Contacted | Qualified | Converted | Closed]
  ← segmented chips, horizontal scroll
[Source chips row (client-side filter): All | Website | Referral | Walk-in | Phone | Campaign | Other]
[My leads toggle chip (client-side, assignedTo == me)]
[ListView.builder of LeadListTile]
[FAB: New lead]

Data contract

  • GET /crm/leads?page=1&limit=20&status=new (crm.controller.ts:37-45); response {data: Lead[], meta: {page, limit, totalItems, totalPages, hasNext, hasPrevious}}.
  • Status values: new|contacted|qualified|converted|closed (lead.schema.ts:7-13).
  • Server sort fixed createdAt: -1 (lead.repository.ts:31).

Row composition

LeadListTile: 40 dp AppAvatar (initials of firstName/lastName), title = full name, subtitle = email (or phone if no email) + "next follow-up 12 Aug" derived from earliest uncompleted followUps[].scheduledAt (lead.schema.ts:28-31), trailing = LeadStatusChip + LeadSourceIcon. Converted rows show a check icon instead of the action hint. Overdue follow-ups render subtitle in color.error with an alert icon.

States

StateUI
initial/loading8 × AppSkeleton list rows
empty (filtered)AppEmptyState "No leads in this status" + clear-filter action
empty (all)AppEmptyState "No leads yet — create your first lead" + CTA to S3
error offlineAppOfflineBanner + cached list (SWR, 00-shared/06 §3.3)
error 5xxAppErrorState(code, message, retry)
permissiontab hidden without crm.read (route guard)

Interactions

  • Status chip tap → server request with status param; chips reflect loading per-tab (keep list, show linear progress).
  • Source / "My leads" chips → client-side filter of current page (no server support — 04 §6 gap; (planned) server filters).
  • Infinite scroll: loadMore() when hasNext (00-shared/06 §3.2).
  • Pull-to-refresh bypasses cache.
  • Realtime (forward-looking): no WS topic for lead changes — no live updates; refresh on foreground resume.

a11y

Row = single semantics label: "Aditi Sharma, contacted, follow-up due 12 Aug". Status chips announce state + count. Contrast for overdue text ≥ 4.5:1 (00-shared/09).

Motion

List fade-slide 150 ms; chip selection m-fast; FAB press scale.

Analytics (proposed)

crm.leads.list, crm.leads.filter(status|source|mine), crm.leads.open(id), crm.leads.load_more.


S2 — Lead Detail (/crm/leads/:id)

Purpose

The lead's full record + follow-up timeline + convert entry point.

Layout (scroll: CustomScrollView with slivers)

  1. Identity header (sticky): AppAvatar 56 dp, name, email, phone, LeadSourceIcon
    • label, LeadStatusChip.
  2. Pipeline card (AppCard):
    • status label + updatedAt
    • if converted: convertedAt, "Student #…" link (convertedToStudentId, lead.schema.ts:85-89)
    • if closed: closedAt + closedReason (lead.schema.ts:91-95)
  3. Placement card: grade / section / academic year / class labels (resolved from refs; unresolved → "—"). Empty state: "Add grade, academic year and class to enable conversion" (mirrors server rule crm.service.ts:145-149).
  4. Assignment card: assignedTo staff name or "Unassigned" + assign action (PATCH assignedTo, update-lead.dto.ts:72-75).
  5. Follow-up timeline (module AppTimeline):
    • each entry: note, scheduledAt (formatted), checkmark when completedAt set, "by {createdBy name}" (lead.schema.ts:24-39)
    • overdue entries in color.error; today's in color.primary
    • empty state: "No follow-ups yet"
  6. Notes card: notes or empty.
  7. Bottom action bar (sticky, crm.lead.manage): Add follow-up, Edit, Convert to student (primary, disabled when converted/closed or placement incomplete — tooltip explains rule).

Data contract

GET /crm/leads/:id (crm.controller.ts:53-57) → 404 RESOURCE_NOT_FOUND for missing/cross-tenant (scoped filter, base.repository.ts:20-30).

States

loading skeleton; 404 → AppErrorState "Lead not found" + back; offline → cached detail (no client cache per 00-shared/06 §3.3 — detail views fetch; banner + retry).

Interactions

  • Convert tap → S4 dialog (rules pre-checked client-side).
  • Follow-up add → bottom sheet (S2-inline form, 08 §2b), POST crm.controller.ts:65-69, appends to timeline on success (pessimistic — server response is authoritative).
  • Edit → S3 sheet; on save PATCH (crm.controller.ts:59-63) → refresh detail.
  • Assignment change → PATCH, optimistic with rollback (00-shared/06 §3.5).

a11y

Timeline announced per entry with date; convert button disabled state explained via Tooltip + semantics hint.

Motion

Section reveal m-base; timeline entries stagger 40 ms.

Analytics (proposed)

crm.lead.open(id), crm.lead.followup.added, crm.lead.assigned, crm.lead.update.completed, crm.lead.convert.opened.


S3 — Lead Editor (Create /crm/leads/new · Edit sheet /crm/leads/:id/edit)

Purpose & entry

Minimal capture (create) / full edit (edit). Create entered from S1 FAB; edit from S2 action bar.

Layout

Bottom sheet (phone) / centered dialog ≤ 560 dp (tablet, 00-shared/05). Scrollable form: 3 required fields first (firstName, lastName, email), then optional groups (contact, source, status, placement, assignment, notes). Submit = primary filled button full width; secondary "Cancel".

Data contract

  • Create: POST /crm/leads (crm.controller.ts:47-51) — payload per create-lead.dto.ts:11-73.
  • Edit: PATCH /crm/leads/:id (crm.controller.ts:59-63) — partial fields only; server merges defined fields (crm.service.ts:88-100); version increments (base.repository.ts:63).
  • Duplicate email → 409 DUPLICATE_RESOURCE (crm.service.ts:42-47).

Field table (exact, per DTO)

FieldRequiredType/validationDefaultSource
firstNamestringcreate-lead.dto.ts:12-14
middleNamestring:16-19
lastNamestring:21-23
email@IsEmail, lowercased server-side:25-27; lead.schema.ts:52-53
phonestring:29-32
sourceenum website/referral/walk_in/phone/campaign/otherwebsite:34-37; lead.schema.ts:58-59
statusenum new/contacted/qualified/converted/closednew:39-42; lead.schema.ts:61-62
gradeIdMongoId:44-47
sectionIdMongoId:49-52
academicYearIdMongoId:54-57
classIdMongoId:59-62
notesstring (multiline):64-67
assignedToMongoId (staff):69-72

Behaviors

  • Create: status hidden by default (server defaults new); exposed under "Advanced" — admission staff may pre-set contacted/qualified.
  • Status rules (mirror server): setting converted via form is blocked — must use convert flow (server guards, crm.service.ts:120-125); setting closed without closedReason → client shows reason field (server auto-stamps closedAt, crm.service.ts:94-96).
  • 409 handling: inline banner "A lead with this email already exists" + actions "View existing" (navigates to existing lead, requires server lookup (planned) — no email-search endpoint; today: user edits email) / "Edit details".
  • 400: per-field errors under fields (00-shared/06 §5).
  • Double-submit guard: submit disabled while in-flight (00-shared/08 §6).

Keyboard & a11y

Email .emailAddress, phone .phone, next-field tab order; labels linked; first invalid field focused on error; live region for 409 banner.

Motion

Sheet slide-up m-base; error shake; success → close + snackbar "Lead created".

Analytics (proposed)

crm.lead.create.submit|success|duplicate, crm.lead.update.submit|success.


S4 — Convert Lead Dialog (from S2)

Purpose

One-action promotion of a qualified lead to User + Student. Irreversible.

Layout

AppDialog: title "Convert to student?", body summary (name, email, grade/year/class), warning list of pre-conditions, actions Cancel / Convert (filled, destructive-tone color.primary). Progress: in-dialog spinner + "Creating user and student record…".

Data contract

POST /crm/leads/:id/convert (crm.controller.ts:71-75).

  • 400 "Lead is already converted." (crm.service.ts:120-122)
  • 400 "Cannot convert a closed lead." (crm.service.ts:123-125)
  • 400 "Lead must have grade, academic year, and class assigned for conversion." (crm.service.ts:145-149)
  • Success: lead becomes converted, convertedAt, convertedToStudentId (crm.service.ts:161-164); emits LeadConverted (crm.service.ts:166-173).

Behaviors

  • Client pre-check disables Convert with tooltip when a rule fails — server 400s are still handled (rule drift) with the exact server message mapped to i18n.
  • No optimistic update (side effects — user/student creation; 00-shared/06 §3.5).
  • On success: dialog → success check → S2 refreshes showing converted badge + student link; snackbar "Student created".

a11y

Dialog announced; pre-condition list read in order; focus lands on Cancel.

Analytics (proposed)

crm.lead.convert.submit, crm.lead.convert.success, crm.lead.convert.failed(rule).


S5 — Admissions List (/crm/admissions)

Purpose

Application funnel working screen with header stats.

Layout (mobile)

[AppBar: "Admissions"]
[FunnelStatRow: 3-4 stat cards (Submitted | Pending docs | Approved | Conversion rate)]
[Status filter chips: All + 9 statuses, horizontal scroll]
[ListView.builder of AdmissionListTile]
[FAB: New application]

Stat cards source: GET /crm/admissions/stats (crm.controller.ts:97-101) → counts per status + total + conversionRate (admission.service.ts:227-237). Tiles show skeleton while loading; failure → tile shows "—" (non-blocking).

Row composition

Name (title), email + submittedAt (subtitle), trailing StatusBadge; document-pending applications get a small "2 docs missing" hint only where documents list known (client-side count from documents, admission.schema.ts:124-125 — actual required set is (planned)).

Data contract

GET /crm/admissions?page&limit&status (crm.controller.ts:103-111); sort fixed submittedAt: -1 (admission.repository.ts:29).

States & interactions

Same pattern as S1: skeleton/empty/error/offline; status chips server-driven; pull-to-refresh refreshes list and stats. Realtime (forward-looking): no WS topic; foreground-resume refetch.

a11y / motion

As S1; stat cards are single semantics labels.

Analytics (proposed)

crm.admissions.list, crm.admissions.filter(status), crm.admissions.stats.view.


S6 — Admission Detail (/crm/admissions/:id)

Purpose

The application record with documents, interview, workflow audit trail, and decidable/convert actions.

Layout (scroll)

  1. Identity header: name, email, phone, StatusBadge.
  2. Pipeline card: status, submittedAt, decidedAt (admission.schema.ts:136-143).
  3. Placement card: grade/academicYear/class (resolved).
  4. Documents card: rows of DocumentCard (type chip — tc/marksheet/certificate/ photo/other, admission.schema.ts:27-33 — filename, uploadedBy, uploadedAt) + Add document action (crm.lead.manage).
  5. Interview card: scheduledAt, mode (online/offline), panel names, feedback (admission.schema.ts:69-81); Schedule action when none / editable.
  6. Workflow timeline (module AppTimeline, authoritative audit trail): each stage "from → to" (admission.schema.ts:52-67), approver name, comment, timestamp — read-only, rendered bottom-up.
  7. Conversion card (when converted): conversion.studentId + convertedAt (admission.schema.ts:83-89) + student link.
  8. Notes card.
  9. Bottom action bar (contextual):
    • status decidable (ADMISSION_DECIDABLE_STATUSES, admission.schema.ts:20-25): Add document, Schedule interview, Make decision (primary)
    • status approved: Convert to student (primary)
    • closed statuses: no action bar; read-only banner "This application is closed."

Data contract

GET /crm/admissions/:id (crm.controller.ts:113-117); 404 for missing/cross-tenant.

Behaviors

  • Every mutation re-fetches detail (workflow + status authoritative from server).
  • Edit admission (PATCH, crm.controller.ts:119-123) available while open — edits allowed on identity/placement/notes only via Edit action; closed → 400 (admission.service.ts:257-269).
  • Decision/interview/document actions route to S8/S9 sheets (below).

a11y / motion

Workflow timeline announced per stage; decision primary button announced with rule hint when disabled. Motion: section reveal m-base.

Analytics (proposed)

crm.admission.open, crm.admission.document.added, crm.admission.interview.scheduled, crm.admission.edit.completed, crm.admission.decided.{approve,reject,waitlist}, crm.admission.converted.


S7 — Admission Editor — Create (/crm/admissions/new)

Purpose & layout

Application intake form (office-facilitated). Full-page scroll form (not sheet — longer form), groups: Applicant (firstName*, lastName*, email*, phone) · Placement (gradeId, academicYearId, classId) · Notes. Submit "Submit application".

Data contract

POST /crm/admissions (crm.controller.ts:91-95). Server forces status submitted + workflow draft → submitted regardless of DTO (admission.service.ts:41-51) — client shows "Application submitted" success state, not a draft.

Fields (exact, create-admission.dto.ts:4-46)

firstName*, lastName*, email* (@IsEmail), phone?, gradeId?, academicYearId?, classId?, notes? — all placement refs MongoId.

Behaviors

  • 400 per-field errors; 409 n/a (no uniqueness on admissions email today — duplicates allowed; flag as possible gap).
  • Success → S5 list with snackbar + new admission visible (sort by submittedAt desc).

Analytics (proposed)

crm.admission.create.submit|success.


S8 — Interview Scheduler (sheet on S6)

Purpose

Set interview slot + panel + mode (+ optional feedback now or later via edit).

Layout

Bottom sheet: AppDatePicker+time (scheduledAt*), panel multi-select chips (staff list (planned) source: staff module read), mode AppSegmentedButton online/offline (default offline — admission.schema.ts:76-78), feedback textarea (optional), actions Cancel / Schedule.

Data contract

POST /crm/admissions/:id/schedule-interview (crm.controller.ts:134-141; schedule-interview.dto.ts:11-31). Server stores interview + transitions to interview_scheduled when different (admission.service.ts:130-132).

Behaviors

  • Re-schedule allowed while open (overwrites interview doc, admission.service.ts:124-129).
  • No optimistic write; success → S6 interview card refreshes + status badge updates.

Analytics (proposed)

crm.admission.interview.scheduled.


S9 — Decision Dialog (on S6)

Purpose

Approve / Reject / Waitlist with optional comment. Terminal for the pipeline — closed statuses become immutable (admission.service.ts:257-269).

Layout

AppDialog: three action choices (AppSegmentedButton or radio list: Approve / Reject / Waitlist), comment textarea, warning line per choice (reject: "Application closes permanently"), Cancel / Confirm decision.

Data contract

POST /crm/admissions/:id/decision (crm.controller.ts:143-147; admission-decision.dto.ts:10-18). Rules:

  • Only from decidable statuses → else 400 with exact message (admission.service.ts:141-145).
  • Sets status + decidedAt (admission.service.ts:155); workflow stage appended with approver + comment (admission.service.ts:239-255).
  • Emits AdmissionApproved / AdmissionRejected (admission.service.ts:158-168); waitlist emits none.

Behaviors

  • Client pre-check: dialog only offered when decidable; server 400 mapped to i18n.
  • Confirm requires explicit selection; default none.
  • Pessimistic; success → S6 status + workflow refresh.

a11y

Warning copy per decision read by screen reader; focus on first choice.

Analytics (proposed)

crm.admission.decided.{approve,reject,waitlist}.


S10 — Campaigns List (/crm/campaigns)

Purpose

Marketing campaign registry.

Layout

AppBar "Campaigns" · ListView.builder of CampaignCard: name (title), type icon + label, StatusBadge (draft/active/paused/completed — campaign.schema.ts:7-12), date range, metrics line "12 leads · 3 converted" (from metrics, campaign.schema.ts:45-52 — values schema-only; write path (planned), show "—" when absent) · FAB "New campaign".

Data contract

GET /crm/campaigns?page&limit (crm.controller.ts:77-81); sort createdAt: -1 (campaign.repository.ts:25).

States & interactions

Standard skeleton/empty/error/offline; no detail route (no GET by id) — tap opens a read-only info bottom sheet (same data); row is informational. Pull-to-refresh.

Analytics (proposed)

crm.campaigns.list, crm.campaign.open.


S11 — Campaign Editor — Create (/crm/campaigns/new)

Purpose

Register a campaign.

Layout

Bottom sheet: name* (required, create-campaign.dto.ts:5-8), description (multiline), type dropdown (email/sms/social/print/other, default email — campaign.schema.ts:30), status dropdown (draft/active/paused/completed, default draft — campaign.schema.ts:33), startDate + endDate pickers (optional), Cancel / Create.

Data contract

POST /crm/campaigns (crm.controller.ts:83-87; create-campaign.dto.ts:5-34). No PATCH/GET-by-id — editing a campaign is (planned).

Behaviors

  • Date sanity: endDate ≥ startDate (client validation; server stores as-is today — no server check, flag).
  • Success → S10 refresh + snackbar.

Analytics (proposed)

crm.campaign.create.submit|success.