diff --git a/app/api/report/route.ts b/app/api/report/route.ts index 0301f54..bd01aca 100644 --- a/app/api/report/route.ts +++ b/app/api/report/route.ts @@ -158,15 +158,26 @@ export async function GET(req: NextRequest) { const orgId = orgs[0].contact_id_b; // Org name + activity walk + option groups, in parallel. + // For file-type fields, also request the joined `.file_name` so the + // report can surface a human-readable filename rather than the raw + // file id that APIv4 returns by default. const civiFieldNames = Array.from( new Set(allFields.map((f) => f.civiField).filter((f): f is string => !!f)), ); + const fileFieldRefs = Array.from( + new Set( + allFields + .filter((f) => f.type === "file" && f.civiField) + .map((f) => `${f.civiField!}.file_name`), + ), + ); const select = [ "id", "activity_date_time", "subject", ACTIVITY_STAGE_FIELD, ...civiFieldNames, + ...fileFieldRefs, ]; const [orgRes, activityRes, options] = await Promise.all([ @@ -227,10 +238,15 @@ export async function GET(req: NextRequest) { for (const row of rows) { const v = row[f.civiField]; if (v === null || v === undefined || v === "") continue; + let value: unknown = v; + if (f.type === "file") { + const fname = row[`${f.civiField}.file_name`]; + value = { id: v, file_name: typeof fname === "string" ? fname : undefined }; + } entries.push({ activityId: row.id, date: row.activity_date_time, - value: v, + value, }); } if (entries.length > 0) fieldHistory[f.name] = entries; diff --git a/components/ReportView.tsx b/components/ReportView.tsx index 50ec593..83a6123 100644 --- a/components/ReportView.tsx +++ b/components/ReportView.tsx @@ -459,8 +459,18 @@ function FormattedValue({ const labels = parts.map((p) => opts?.find((o) => o.value === p)?.label ?? p); return <>{labels.join(", ")}; } - case "file": + case "file": { + // Prefill / history wraps file values into { id, file_name } so the + // UI can show a human-readable name. Fall back to whatever scalar + // came through if the shape is different. + if (typeof value === "object" && value !== null) { + const o = value as Record; + const fname = o.file_name ?? o.name ?? o.label ?? o.filename; + if (typeof fname === "string" && fname.length > 0) return <>{fname}; + if (typeof o.id !== "undefined") return <>Attachment #{String(o.id)}; + } return <>{String(value)}; + } case "textarea": case "text": case "email": diff --git a/lib/prefill.ts b/lib/prefill.ts index be09714..5d3ae29 100644 --- a/lib/prefill.ts +++ b/lib/prefill.ts @@ -39,7 +39,19 @@ export async function loadPrefill( activityTypeName = "Org Engagement Submission", ): Promise { const civiSelected = fields.filter((f) => f.civiField); - const select = ["id", "activity_date_time", ...new Set(civiSelected.map((f) => f.civiField!))]; + + // CiviCRM File custom fields return a file id by default. To surface a + // human-readable filename in the prefill (so the prior-attachment + // indicator can show it), also request the joined `.file_name` for any + // file-type field. + const fileFieldRefs = new Set( + civiSelected.filter((f) => f.type === "file").map((f) => f.civiField!), + ); + + const selectSet = new Set(["id", "activity_date_time"]); + for (const f of civiSelected) selectSet.add(f.civiField!); + for (const ref of fileFieldRefs) selectSet.add(`${ref}.file_name`); + const select = Array.from(selectSet); const res = await civi("Activity", "get", { select, @@ -56,10 +68,17 @@ export async function loadPrefill( for (const f of civiSelected) { for (const row of rows) { const v = row[f.civiField!]; - if (v !== null && v !== undefined && v !== "") { + if (v === null || v === undefined || v === "") continue; + if (f.type === "file") { + const fname = row[`${f.civiField!}.file_name`]; + out[f.name] = { + id: v, + file_name: typeof fname === "string" ? fname : undefined, + }; + } else { out[f.name] = v; - break; } + break; } }