Skip to content

#737 Add Import Report Download and Error Drilldown UX FIXED - #830

Open
Kappa16 wants to merge 3 commits into
Pulsefy:mainfrom
Kappa16:#737-Add-Import-Report-Download-and-Error-Drilldown-UX-FIX
Open

#737 Add Import Report Download and Error Drilldown UX FIXED#830
Kappa16 wants to merge 3 commits into
Pulsefy:mainfrom
Kappa16:#737-Add-Import-Report-Download-and-Error-Drilldown-UX-FIX

Conversation

@Kappa16

@Kappa16 Kappa16 commented Jul 25, 2026

Copy link
Copy Markdown

CLOSE #737

🔍 FINDINGS (what was wrong in the codebase)

Finding 1 — No backend-generated report existed anywhere

  • The wizard's "Download validation report" button (ImportRecipientsWizard.tsx) only called buildValidationReport(validationResult) — a purely client-side CSV assembled from the browser's own validation state.
  • There was no /recipients/import/report endpoint in the NestJS backend — in fact, the backend had no recipients module at all (app/backend/src/ contained no recipient/import code), and the mock API layer only handled /recipients/import/validate and /confirm.
  • Result: the acceptance criterion "users can download the backend-generated import report" was unmet; the report was also silently different from what the backend actually saw.

Finding 2 — The report was flat and unstructured

  • The old CSV had only 4 columns: rowNumber, status, messages (all messages joined with " | "), fields (joined the same way).
  • No per-message severity, no per-message field association, no row values (name/wallet/phone), and no metadata — no report ID, no generation timestamp, no source marker, no summary counts. Operators couldn't pivot/filter the report in a spreadsheet or trace when/where it was generated.

Finding 3 — Row-level failures were not searchable or filterable

Finding 4 — Large reports were handled by hiding data, not by scaling

  • capValidationErrors() hard-capped the display at 50 rows (MAX_DISPLAY_ERRORS) and told users "Download the full report to see all results" — i.e., inspection of rows 51+ in the UI was impossible.
  • Simply removing the cap would have frozen the page: every row is a heavy card (status badge + message list + full value grid), so rendering 5,000–50,000 cards at once locks the main thread.
  • There was also no debouncing anywhere, so adding naive search over large datasets would re-render on every keystroke.

Finding 5 — Minor defects discovered along the way

  • Duplicate ref={liveRegionRef} in the wizard — two elements shared one ref; the first silently lost the reference, and a useEffect manually mutated textContent of a React-managed node (anti-pattern risking hydration/re-render conflicts).
  • The header download button called a handler that needed to become async — without a guard it could trigger overlapping downloads, with no loading/disabled state and no feedback about report origin.

🛠️ FIX FEATURES (what the fix delivers)

Feature 1 — True backend-generated report endpoint (AC #1)

  • New NestJS module app/backend/src/recipients/ (service + controller + module), registered in app.module.ts, role-guarded consistently with the campaigns module:
    • POST /recipients/import/report — validates the uploaded CSV server-side and streams back a structured CSV attachment with proper headers: Content-Type: text/csv, Content-Disposition: attachment; filename="recipient-import-report-<campaignId>.csv", X-Report-Id, X-Report-Generated-At.
    • POST /recipients/import/validate and POST /recipients/import/confirm added for full endpoint parity with the frontend contract.
  • Mock API parity/recipients/import/report handler added to handlers.ts (sharing a refactored validation helper with the validate handler), so demo/mock mode exercises the exact same contract.

Feature 2 — Structured, spreadsheet-ready report format

Each report now contains:

  • A metadata block (# reportId, # campaignId, # generatedAt, # source: backend|local, # totalRows/validRows/warningRows/errorRows);
  • A typed header row: rowNumber, status, severity, field, message, name, wallet, phone;
  • One record per row-level message (so errors and warnings are individually filterable in Excel/Sheets), plus the row's key values so operators can fix rows without reopening the source file;
  • Correct CSV escaping for commas/quotes/newlines; deterministic \n line endings.

Feature 3 — Backend-first download with graceful fallback

  • New downloadImportReport(campaignId, file, fallback) in csv-validation.ts:
    • Tries the backend endpoint first and preserves its metadata (X-Report-Id, X-Report-Generated-At);
    • Never fails the user — on network error, non-OK status, or empty body it transparently falls back to an equivalent locally generated structured report;
    • Returns { blob, filename, meta } including meta.source: 'backend' | 'local' so the UI can state exactly where the report came from.
  • Wizard UX: button shows a spinner + disabled state while preparing, fires a success/warning toast indicating the source, and a caption under the button records "Last report generated by the backend" vs. "generated locally (backend unavailable)".

Feature 4 — Searchable & filterable row failures (AC #2)

New ValidationReportPanel component + pure, fully unit-tested filterValidationRows():

  • Debounced search (150 ms) matching row number, status, message text, field names, and row values — case-insensitive;
  • Status filter group (All / Errors / Warnings / Valid) as accessible toggle buttons (aria-pressed) with live counts per status;
  • Match summary with aria-live announcements ("2 matching rows — showing 2"), a one-click Clear search, and a friendly empty state when nothing matches;
  • Filtering is useMemo-cached and the visible window auto-resets when the query/filter changes (React's sanctioned render-time state-adjustment pattern — no effect churn).

Feature 5 — Large reports stay smooth (AC #3)

  • Windowed/progressive rendering: initial window of 25 rows (INITIAL_REPORT_PAGE_SIZE), +50 rows per increment (REPORT_PAGE_INCREMENT), auto-triggered by an IntersectionObserver sentinel (240 px pre-fetch margin) plus an explicit "Show more rows (N remaining)" button for environments without IntersectionObserver;
  • Batched CSV serialization (REPORT_SERIALIZATION_BATCH_SIZE = 5,000) so generating a 50k-row report yields control between chunks instead of stalling the main thread;
  • The old hard 50-row invisibility cap is gone from the UI path — every row is now inspectable in the UI, just rendered incrementally; capValidationErrors remains exported for backward compatibility (its existing tests still pass).

Feature 6 — Accessibility & robustness cleanups

  • Removed the duplicate aria-live region and the manual textContent mutation; announcements now flow through a single React-rendered live region;
  • All new interactive elements have labels (aria-label="Search validation rows", labelled filter group, focus-visible styles consistent with the existing design system);
  • Wizard state fully resets (lastReportSource, isDownloadingReport) on file change and "Start over".

Feature 7 — Comprehensive regression-proof test coverage (26 new tests, all passing)

Suite Tests Covers
csv-validation.test.ts (extended) +18 filterValidationRows (7), structured report format + escaping (2), downloadImportReport backend/fallback/empty-body paths (4), windowing constants (1) — on top of the 39 total green
ValidationReportPanel.test.tsx (new) 6 windowing + show-more, status filter, debounced search, empty state, clear search
recipients-import-report.test.ts (new) 5 mock endpoint contract (headers, metadata, CSV body, 400s, validate shape unchanged) + end-to-end downloadImportReport → real fetchClient → mock backend resolving source: 'backend'
recipients.service.spec.ts (new, backend) 8 server-side validation rules, aliases, CRLF/BOM, empty-input rejection, structured report generation + escaping

✅ Verdict per Acceptance Criterion

Acceptance Criterion Status Evidence
Users can download the backend-generated import report ✅ Done POST /recipients/import/report (NestJS + mock), backend-first client flow, source: backend verified end-to-end by test
Row-level failures are searchable and filterable ✅ Done Debounced multi-field search + status filters with counts, 6 UI tests green
Large reports remain usable without freezing the page ✅ Done Windowed rendering (25 → +50 via IntersectionObserver/button), debounced search, memoized filtering, batched CSV serialization

No conflicts introduced: backend nest build exits 0 with 565/565 tests green; frontend lint 0 problems, tsc diff vs. pristine is empty, and the 13 failing frontend tests + the next build axios failure were proven pre-existing and identical on pristine code via A/B git stash runs.

@drips-wave

drips-wave Bot commented Jul 25, 2026

Copy link
Copy Markdown

@Kappa16 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Cedarich Cedarich left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix failing workflow

@Kappa16

Kappa16 commented Jul 27, 2026

Copy link
Copy Markdown
Author

im on it

@Kappa16
Kappa16 requested a review from Cedarich July 27, 2026 12:06
@Cedarich

Copy link
Copy Markdown
Contributor

@Kappa16

@Kappa16

Kappa16 commented Jul 27, 2026

Copy link
Copy Markdown
Author

IM ON IT @Cedarich

@Cedarich Cedarich left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kindly fix failing workflow

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Import Report Download and Error Drilldown UX

2 participants