-
+ {columns.map(column => (
+
- {labels[column] || report.labels[column] || column.replace(/_/g, ' ')} +
- {formatFormValue(row[column])} +
diff --git a/docs/forms-reporting.md b/docs/forms-reporting.md
index 2e1926737..bd0fca130 100644
--- a/docs/forms-reporting.md
+++ b/docs/forms-reporting.md
@@ -5,10 +5,14 @@ on the combined host and `/forms` on the dedicated Reports host. The API continu
to enforce its own Administrator / Forms Reporter / `read:forms-submissions`
authorization. Talent Manager access does not expose submission PII.
-Select a named form (the contact form is `Let’s talk`) to load 25 rows
-per page. Columns include every field across published and retired revisions, plus
-submission time, source page, member identity, and revision metadata. Drafts are
-excluded from the selector. Empty forms and failed requests have explicit UI states.
+Select a named form (the contact form is `Let’s talk`) to load 25 submissions
+per page, newest first (submission time descending, then UUID descending for ties).
+Let’s talk uses responsive cards with contact details, member ID and UTM values
+above the full-width inquiry, preserving line breaks and wrapping long text/URLs.
+The cards omit source page, form version, Leadsource, Landing Page Link, Lead Source
+For Campaign C, Primary Campaign C and Insta Page Name C. Other forms retain tables
+with every field across published and retired revisions and submission metadata.
+Drafts are excluded from the selector. Empty forms and failed requests have explicit UI states.
Start/end dates include the entire UTC day. Either boundary may be omitted. Changing
forms or dates resets pagination; reversed ranges are rejected locally and by the
@@ -17,6 +21,7 @@ navigate with API cursors and the result shows the total matching submissions.
**Export all CSV** ignores date inputs; **Export filtered CSV** applies them. Both
export all matching submissions regardless of the current table page. The API
+retains all fields in CSV, including those hidden from the contact cards, in newest-first order. It
streams batches with one header and spreadsheet-safe escaping. The browser receives
the result as a Blob, so very large downloads are bounded by browser memory.
@@ -29,6 +34,8 @@ the result as a Blob, so very large downloads are bounded by browser memory.
Deploy the companion forms-api-v6 reporting change before the portal. Add the exact
Reports/combined portal origins to the API CORS allowlist; website-only CORS is
insufficient. No new UI secrets or environment variables are needed.
+Deploy the companion website change to capture URL UTM values and authenticated
+member IDs on new Let’s talk submissions. Previously missing values remain empty.
Validation: `yarn lint`, `yarn run build`, and focused Forms page/service and Reports
navigation/route tests via `yarn test:no-watch --runInBand --testPathPattern=apps/reports/src`.
diff --git a/src/apps/reports/src/pages/forms/FormsPage.module.scss b/src/apps/reports/src/pages/forms/FormsPage.module.scss
index adba37200..ac88c2ede 100644
--- a/src/apps/reports/src/pages/forms/FormsPage.module.scss
+++ b/src/apps/reports/src/pages/forms/FormsPage.module.scss
@@ -25,6 +25,33 @@
}
.filters label { display: flex; flex-direction: column; gap: 8px; max-width: 100%; }
.filters select { max-width: 100%; }
+.submissions {
+ display: grid;
+ gap: 16px;
+}
+.submission {
+ min-width: 0;
+ padding: 20px;
+ border: 1px solid #d4d4d4;
+ border-radius: 8px;
+ background: #fff;
+}
+.details {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
+ gap: 20px 24px;
+ margin: 0;
+
+ > div { min-width: 0; }
+ dt { font-size: 14px; font-weight: 600; color: #555; margin-bottom: 6px; }
+ dd { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
+}
+.inquiry {
+ grid-column: 1 / -1;
+ border-top: 1px solid #d4d4d4;
+ padding-top: 16px;
+ line-height: 1.6;
+}
.tableScroll {
overflow-x: auto;
table { border-collapse: collapse; width: 100%; }
diff --git a/src/apps/reports/src/pages/forms/FormsPage.spec.tsx b/src/apps/reports/src/pages/forms/FormsPage.spec.tsx
index 5d92968fe..fe9609026 100644
--- a/src/apps/reports/src/pages/forms/FormsPage.spec.tsx
+++ b/src/apps/reports/src/pages/forms/FormsPage.spec.tsx
@@ -1,6 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports, unicorn/no-null */
import '@testing-library/jest-dom'
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import FormsPage from './FormsPage'
import { fetchReportForms, fetchFormSubmissions, downloadFormSubmissions } from '../../lib/services/forms.service'
@@ -58,6 +58,67 @@ describe('Forms reporting page', () => {
.toBeDisabled()
})
+ it('shows contact details and UTM values above full-width inquiries, hiding repetitive fields', async () => {
+ const hidden = ['source_page', 'form_version', 'leadsource', 'source__c',
+ 'lead_source_for_campaign__c', 'primary_campaign__c', 'insta_page_name__c']
+ const inquiry = `A detailed request.\nAnother paragraph with a very long link: ${'x'.repeat(500)}`;
+ (fetchFormSubmissions as jest.Mock).mockResolvedValue({
+ ...report,
+ columns: [
+ 'submitted_at', 'member_id', 'email', 'briefly_describe_your_inquiry', 'utm_source__c', ...hidden,
+ ],
+ data: [
+ { ...report.data[0],
+ briefly_describe_your_inquiry: inquiry,
+ member_id: '12345',
+ submitted_at: '2026-10-19T12:00:00Z',
+ utm_source__c: 'newsletter' },
+ { email: 'older@example.com', submission_id: 'two', submitted_at: '2026-10-18T12:00:00Z' },
+ ],
+ labels: { ...report.labels, briefly_describe_your_inquiry: 'What can we help you with?' },
+ })
+ render(
{`${report.total} submissions · Page ${page}`}
- {report.data.length === 0 ?No submissions match these dates.
: ( + {report.data.length === 0 ? ( +No submissions match these dates.
+ ) : report.form === 'lets_talk' ? ( +