diff --git a/src/app/lib/parseMeasures.ts b/src/app/lib/parseMeasures.ts new file mode 100644 index 0000000..06bf73b --- /dev/null +++ b/src/app/lib/parseMeasures.ts @@ -0,0 +1,20 @@ +/** + * Parses a measure-list string from the HubSpot pull pipeline. + * + * HubSpot's `proposed_measures` field is delivered as a semicolon-separated + * list (its native multi-select format). Earlier records were sometimes stored + * as comma-separated strings, and freeform text from contractors may use either + * separator. To keep the UI tolerant we accept both `;` and `,` as delimiters. + * + * Empty / whitespace-only inputs return `[]`. Individual entries are trimmed + * and any blank entries (e.g. from a trailing separator) are dropped. + */ +export function parseMeasures(raw: string | null | undefined): string[] { + if (!raw) return []; + // Tolerant strategy: split on either `;` or `,`. HubSpot's multi-select + // exports use `;` natively; legacy values and ad-hoc text may use `,`. + return raw + .split(/[;,]/) + .map((m) => m.trim()) + .filter(Boolean); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx index ccb1db8..a7dd151 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/ContractorUploadModal.tsx @@ -23,6 +23,7 @@ import { CheckCircle2, XCircle, Upload, Loader2, Clock, ChevronDown, ChevronRigh import { uploadFileToS3 } from "@/app/utils/s3"; import type { ClassifiedDeal, DocStatusMap } from "./types"; import { getRequiredDocs } from "@/app/lib/measureDocumentRequirements"; +import { parseMeasures } from "@/app/lib/parseMeasures"; // ── Types ───────────────────────────────────────────────────────────────── @@ -164,11 +165,6 @@ function contentTypeFor(ext: string): string { return "application/octet-stream"; } -function parseMeasures(raw: string | null | undefined): string[] { - if (!raw) return []; - return raw.split(",").map((m) => m.trim()).filter(Boolean); -} - function s3KeyBasename(key: string): string { return key.split("/").pop() ?? key; } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/MeasuresTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/MeasuresTable.tsx index f7e3414..764a6eb 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/MeasuresTable.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/MeasuresTable.tsx @@ -18,6 +18,7 @@ import { Search, Save, ChevronDown, ChevronRight } from "lucide-react"; import { STAGE_COLORS } from "./types"; import type { ClassifiedDeal, PortfolioCapabilityType, ApprovalsByDeal } from "./types"; import { ApprovalConfirmDialog, type PendingDiff } from "./ApprovalConfirmDialog"; +import { parseMeasures } from "@/app/lib/parseMeasures"; type AuditEvent = { id: string; @@ -36,11 +37,6 @@ type Props = { portfolioId: string; }; -function parseMeasures(raw: string | null | undefined): string[] { - if (!raw) return []; - return raw.split(",").map((m) => m.trim()).filter(Boolean); -} - function ApprovalStatus({ proposed, approved,