06 — Screen Specifications (Bulk Module)
- Legend
- SS1 — Bulk Home / Upload
- SS2 — CSV Preview
- SS3 — Confirm
- SS4 — Result & Error Review
- SS5 — Export
- Cross-screen requirements
Detailed specification of every bulk screen: layout, content, states, interactions, error handling, exact copy and backend citations. The biggest document in this package. Components in 07, forms/CSV contract in 08, motion in 00-shared/08, QA in 14. Forward-looking client.
Legend
SSn= screen id (see 05). Routes are(forward-looking).- States: idle / loading / success / empty / error / disabled / permission.
- "Server 400/404" notes cite exact backend behaviour
(
bulk-import.service.ts,bulk.controller.ts).
SS1 — Bulk Home / Upload
1.1 Layout (desktop ≥ 1280 dp)
┌──────────────────────────────────────────────────────────────┐
│ Breadcrumb: Admin › Import & Export [Export →]│
├──────────────────────────────────────────────────────────────┤
│ Card: Import data (width ≤ 720, centered) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Entity [ Students ▾ ] (only option; others │ │
│ │ │ │
│ │ File ╔══════════════════════════════════╗ │ │
│ │ ║ Drop students.csv here or ║ │ │
│ │ ║ [ Browse files ] ║ │ │
│ │ ║ .csv only · ≤ 1000 rows · ≤2 MB║ │ │
│ │ ╚══════════════════════════════════╝ │ │
│ │ [ Expected columns ] ▾ (08 §1 contract summary) │ │
│ │ │ │
│ │ [ Back ] [ Next → ] │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
1.2 Behaviour
- Entity selector: hard-coded
['students']— the only adapter isstudents(students-import.adapter.ts:15); others(planned)(IMPLEMENTATION_PLAN.md:172). Selector present anyway to future-proof; disabled-looking when single option. - File pick:
.csvextension filter; MIME not trusted (server reads bytes,bulk.controller.ts:47). On selection the client immediately parses the file in an isolate (15 §4) mirroring server optionscolumns:true, skip_empty_lines:true, trim:true(bulk-import.service.ts:26-30). - Parse outcomes:
- Success → file card shows name, row count (data rows = lines −
header), detected headers;
Nextenabled. - Unparsable (unclosed quote, binary, garbage) → inline error
"This file could not be read as CSV." (client-side mirror of server 400
Malformed CSV: could not parse file.—bulk-import.service.ts:31-33). - Zero data rows → "CSV must include a header row and data."
(mirror of
:34-35);Nextstays disabled. - Encoding/BOM: strip UTF-8 BOM before parse (
14 §2); non-UTF-8 (e.g. Windows-1252 accents) → warning card with "encode as UTF-8" guidance — the server decodes UTF-8 only (bulk.controller.ts:47).
- Success → file card shows name, row count (data rows = lines −
header), detected headers;
Next→ SS2.Backdisabled on SS1 (it is the flow root).
1.3 States
| State | Visual | Notes |
|---|---|---|
| idle | card, Next disabled | |
| parsing | file card skeleton, spinner | isolate parse is ms-level for ≤ 1000 rows |
| error | AppAlert error under file zone | per 14 §2 encodings |
| permission | empty state "You don't have import permission" | client gate only; server RBAC (planned) (permissions.constants.ts:10) |
| offline | AppOfflineBanner; pick still allowed, Next blocked | no offline import (00-shared/06 §3.5) |
1.4 Analytics (proposed)
bulk.upload.pick(entity, rows), bulk.upload.parse_fail(reason).
SS2 — CSV Preview
2.1 Layout
┌──────────────────────────────────────────────────────────────┐
│ Import › Preview file: students.csv │
│ [ImportStepper: Upload ✓ · Preview ● · Confirm · Result] │
├──────────────────────────────────────────────────────────────┤
│ Header map (chips per column) │
│ firstName [required] · lastName [required] · email [required]│
│ admissionNumber [required] · grade [required] │
│ section [required] · academicYear [required] │
│ rollNumber [optional] · phone [unknown!] │
│ ⚠ 1 unknown column ("phone") will be ignored │
│ ⚠ blocking: missing required column "middleName"? no — │
│ list ACTUAL missing: e.g. missing "grade" │
├──────────────────────────────────────────────────────────────┤
│ # firstName lastName email admissionNumber … │
│ 2 Amara Osei amara.o@school.x A-2026-001 … │
│ 3 Bilal Khan bilal.k@school.x A-2026-002 … │
│ … (first 10 rows; "showing 10 of 413") │
│ [Back] [Import 413 →] │
└──────────────────────────────────────────────────────────────┘
2.2 Header contract logic (client-side, exact to server)
Compare file headers (as read, no case normalization — the server matches
exact names: students-import.adapter.ts:17-26) against:
| State | Classification | Blocking? |
|---|---|---|
| header ∈ required set | [required] chip | — |
header ∈ optional set (rollNumber) | [optional] chip | — |
| header not in contract | [unknown] chip + warning "will be ignored" | no |
| required column absent | AppAlert error listing column name(s) | yes (Next disabled) |
Trim nuance: the server trims cell values (trim:true,
bulk-import.service.ts:29) and the adapter trims values again
(students-import.adapter.ts:41-44) — but headers are not normalized.
A header " firstName" will produce rows keyed " firstName" and every
required check will fail with Missing required column "firstName".
(:43). The client must surface whitespace in header chips ("leading space
detected") as blocking — this is the #1 real-world import failure
(14 §2).
2.3 Preview table
- Shows first 10 data rows; row numbers shown are physical line numbers
(header = 1) so they match server
rowNumberin errors (bulk-import.service.ts:46). - Empty after parse (0 data rows) → blocking state with server-exact copy
CSV must include a header row and data.(:34-35). - Duplicate emails/admissionNumbers within the file: client can warn
(
(forward-looking); server only checks against DB,students-import.adapter.ts:49-56) — flagged as enhancement, not required.
2.4 CTA
Import N rows → SS3. Disabled when any blocking warning exists.
SS3 — Confirm
3.1 Content (exact, honest copy)
Import 413 rows as students
• 413 rows will create 413 user accounts (firstName, lastName, email)
and 413 student records with auto-enrollment into their grade/section
(student.service.ts:71-78).
• Rows whose email or admissionNumber already exist will be skipped and
listed as errors — they are not overwritten (students-import.adapter.ts:49-56).
• Grade/section/academicYear must already exist; unmatched values are
skipped with "not found" errors (students-import.adapter.ts:113-125).
• Re-uploading a file that already succeeded is safe: completed rows fail
as duplicates, new rows import.
• This import runs in one request — keep this tab open until the result
appears (bulk-import.service.ts:22-65). ← large-file caveat
[Back] [Start import]
3.2 Rules
- No new data entry on this screen; pure review.
Start importtriggers the synchronousPOST /bulk/import/students(bulk.controller.ts:35-48) and navigates to SS4 in-flight state immediately — the request must not be awaited before navigating (progress UI lives on SS4;10 §6).- Double-tap protection: CTA disabled once fired (idempotent server, but
double-fire wastes the 100/min api budget —
00-shared/07 §4).
SS4 — Result & Error Review
4.1 States
a) In-flight
┌────────────────────────────────────────────────┐
│ Importing 413 rows… │
│ [Indeterminate progress bar] │
│ • All rows are validated and created server- │
│ side in this single request. │
│ • Do not close this tab; you will see the │
│ report when it finishes. │
└────────────────────────────────────────────────┘
- No progress events exist (synchronous service,
bulk-import.service.ts:22-65); the UI is honest: indeterminate bar + explanation. When async workers land ((planned),IMPLEMENTATION_PLAN.md:172), this becomes a determinate progress + polling seam (13 §4). - Offline/timeout: on network failure the request may have partially
completed server-side (rows already created). Copy: "The connection was
lost. Check your students list, then re-upload — completed rows will be
reported as duplicates, not recreated." (idempotency by design,
students-import.adapter.ts:49-56).
b) Success (imported === totalRows)
┌────────────────────────────────────────────────┐
│ ✓ 413 of 413 rows imported │
│ • 413 users created (in-app notifications │
│ sent via UserCreated → in-app queue │
│ user-created-notification, event-queue-map.ts:10) │
│ • 413 students enrolled (StudentCreated → │
│ in-app student-enrolled, event-queue-map.ts:28) │
│ [View students] [Download report] │
└────────────────────────────────────────────────┘
c) Partial (failed > 0)
┌────────────────────────────────────────────────┐
│ ⚠ 398 of 413 rows imported · 15 failed │
│ [Error groups] Duplicates 9 · References 5 · │
│ Format 1 │
│ ┌─ ErrorReviewTable ──────────────────────────┐ │
│ │ Row │ Fields │ Error │ │
│ │ 17 │ email, admissionNumber │ Email "…" │ │
│ │ │ │ already │ │
│ │ │ │ registered│ │
│ │ 88 │ academicYear │ Academic │ │
│ │ │ │ year "…" │ │
│ │ │ │ not found │ │
│ │ … (paged 50)│ │
│ └──────────────────────────────────────────────┘ │
│ [Download error CSV] [Fix errors →] [View students] │
└────────────────────────────────────────────────┘
- Error rows show: physical row number (matches file line — server
rowNumber = index+2,bulk-import.service.ts:46), affected column(s), full error string(s) — exact server strings in08 §2. - Error CSV is client-synthesized: original row +
errorcolumn (no server endpoint exists). - "Fix errors" → SS1 with failed row numbers pre-queued so the preview
can highlight them after the corrected file is picked (
03 J3). - Duplicate rows are labelled with a "likely already imported" hint on
re-upload journeys (
03 J3), normalizing the duplicate-noise problem.
d) Failed before any row (server rejects the file)
| Server trigger | Code/message | SS4 rendering |
|---|---|---|
| no file field | 400 CSV file is required (multipart field "file"). (bulk.controller.ts:43-46) | client prevents this (always sends file); show generic + retry |
| unknown entity | 404 RESOURCE_NOT_FOUND No import adapter for entity "X". (bulk-import.service.ts:17-20) | "Entity not available" + back to SS1 |
| malformed CSV | 400 VALIDATION_ERROR Malformed CSV: could not parse file. (:31-33) | "File could not be parsed" + back to SS1 (preview normally catches this) |
| header-only CSV | 400 VALIDATION_ERROR CSV must include a header row and data. (:34-35) | same pattern |
e) Rate-limited / permission
- 429 (
RATE_LIMITED, api tier 100/min —00-shared/07 §4): countdown copyRetry in Ns, no auto-retry. - 401: session refresh flow (
00-shared/06 §3.6). - 403: only when RBAC guard lands
(planned); render permission empty state.
4.2 Invariants the UI can assert
imported + failed === totalRows (04 §5). If the client detects a mismatch
it renders a data-error banner with the raw report for support.
4.3 Realtime
No WS topic exists for import results; notifications to imported users are
server-side only. (forward-looking): a bulk.import.completed WS topic once
async workers exist.
SS5 — Export
5.1 Layout
Import & Export › Export
Entity [ Students ▾ ] (hard-coded list; students only)
Format CSV (text/csv) (fixed)
Note: roster snapshot — columns admissionNumber, rollNumber, status,
admissionDate; sorted by admissionNumber (students-import.adapter.ts:87-98).
Not an import template (04 §6).
[Download CSV]
5.2 Behaviour
GET /api/v1/bulk/export/students(bulk.controller.ts:50-60); responsetext/csv+Content-Disposition: attachment; filename="students.csv"(:52,55-58).- Empty roster → header-only CSV (
stringify([], {header:true}),bulk-import.service.ts:67-71); UI shows "No rows exported" toast, still downloads the header file. - Unknown entity → 404 (
:17-20); client prevents via hard-coded list. - The file downloads through the browser; on phone a share sheet
(forward-looking).
5.3 Analytics (proposed)
bulk.export.download(entity).
Cross-screen requirements
| Concern | Rule |
|---|---|
| Stepper | SS1→SS2→SS3→SS4; SS5 outside wizard (ImportStepper, 07) |
| Back | preserves wizard state (file + preview) so users can fix without re-picking when possible |
| Unsaved state | no forms with server writes until SS3; leaving SS3 before firing has no effect |
| Large file | SS1 cap + warning; SS3 caveat; SS4 in-flight honesty (10 §6) |
| a11y | live-region on parse errors and result summary; tables with real semantics (00-shared/09) |
| Motion | m-fast micro-feedback, m-base screen transitions (00-shared/08) |