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",