diff --git a/src/components/sponsors/reports/LinesManifestView.js b/src/components/sponsors/reports/LinesManifestView.js index 475b97127..d828650e0 100644 --- a/src/components/sponsors/reports/LinesManifestView.js +++ b/src/components/sponsors/reports/LinesManifestView.js @@ -80,6 +80,20 @@ export const Destination = ({ name, booth }) => { ); }; +// Values arrive display-ready from the API; reformatting here would desync the +// screen from the CSV. +const AdditionalFields = ({ fields }) => ( + <> + {(fields ?? []).map((field, idx) => ( + {`${field.label}: ${field.value}`} + ))} + +); + // Buckets flat per-line rows into sponsor groups, preserving first-seen order. // // Do NOT rely on row adjacency: the backend orders lines by sponsor NAME @@ -119,6 +133,7 @@ const HEADERS = [ { key: "col_destination" }, { key: "col_checkout_at" }, { key: "col_notes" }, + { key: "col_additional_fields" }, { key: "col_quantity", align: "right" }, { key: "col_used_rate" }, { key: "col_status" }, @@ -229,6 +244,9 @@ const LinesManifestView = ({ {formatCheckoutTime(line.purchase?.checkout_at)} {line.notes} + + + {line.is_partially_canceled ? `${liveQuantity(line)} / ${line.quantity}` diff --git a/src/components/sponsors/reports/__tests__/LinesManifestView.test.js b/src/components/sponsors/reports/__tests__/LinesManifestView.test.js index eb755993d..ec7c41a51 100644 --- a/src/components/sponsors/reports/__tests__/LinesManifestView.test.js +++ b/src/components/sponsors/reports/__tests__/LinesManifestView.test.js @@ -117,11 +117,11 @@ describe("LinesManifestView", () => { // HEADER row, which is a separate array (HEADERS). If the two desync by // one, every column right of the break silently misaligns and stays // green. Assert exact header/cell cardinality directly. - it("has exactly 13 column headers matching 13 cells per row", () => { + it("has exactly 14 column headers matching 14 cells per row", () => { renderView(); - expect(screen.getAllByRole("columnheader")).toHaveLength(13); + expect(screen.getAllByRole("columnheader")).toHaveLength(14); const row = screen.getByText("AV1").closest("tr"); - expect(within(row).getAllByRole("cell")).toHaveLength(13); + expect(within(row).getAllByRole("cell")).toHaveLength(14); }); // Sponsor bucketing (formerly bucketLinesBySponsor, now a private helper). @@ -207,6 +207,55 @@ describe("Destination booth fallback", () => { }); }); +describe("Additional Fields column", () => { + it("renders every answered field as 'Label: value', one per line, in the order received", () => { + renderView({ + rows: [ + line({ + additional_fields: [ + { label: "Chair Color", value: "Blue" }, + { label: "Start Time", value: "08:30" } + ] + }) + ] + }); + const row = screen.getByText("AV1").closest("tr"); + const cells = within(row).getAllByRole("cell"); + // Immediately after Notes (index 6), so index 7. + const cell = cells[7]; + expect(cell).toHaveTextContent("Chair Color: Blue"); + expect(cell).toHaveTextContent("Start Time: 08:30"); + // Order received, not sorted: the backend preserves the source snapshot's order + // and the logistics manifest is read in that order. + expect(cell.textContent.indexOf("Chair Color")).toBeLessThan( + cell.textContent.indexOf("Start Time") + ); + // One element per entry, so each renders on its own line rather than as one run-on string. + expect(cell.querySelectorAll("div")).toHaveLength(2); + }); + + it("renders an empty cell for a null, missing or empty additional_fields", () => { + // Three shapes the API can legitimately send. A crash here takes down the whole + // report page, so all three are pinned in one test rather than left to chance. + [ + line({ additional_fields: null }), + line({ additional_fields: [] }), + (() => { + const l = line(); + delete l.additional_fields; + return l; + })() + ].forEach((row) => { + const { unmount } = renderView({ rows: [row] }); + const cells = within(screen.getByText("AV1").closest("tr")).getAllByRole( + "cell" + ); + expect(cells[7]).toBeEmptyDOMElement(); + unmount(); + }); + }); +}); + describe("lines_count copy", () => { it("says the count is of LIVE lines, not all rendered lines", () => { // The chip is fed liveLineCount, but canceled lines still RENDER, so a group diff --git a/src/i18n/en.json b/src/i18n/en.json index f67120702..be86b43e0 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -4397,6 +4397,7 @@ "col_destination": "Destination", "col_checkout_at": "Checked Out At", "col_notes": "Notes", + "col_additional_fields": "Additional Fields", "col_quantity": "Qty", "col_sponsor": "Sponsor", "col_sponsor_note": "Sponsor Note",