diff --git a/components/StaffReportView.tsx b/components/StaffReportView.tsx index c53e7c6..efe2643 100644 --- a/components/StaffReportView.tsx +++ b/components/StaffReportView.tsx @@ -121,6 +121,16 @@ export function StaffReportView({ const membersField = checkInSection?.fields.find((f) => f.descriptor.name === MEMBERS_ACTUAL_NAME); const goalField = checkInSection?.fields.find((f) => f.descriptor.name === MEMBERS_GOAL_NAME); + // Most recent Survey_completed_by / _email values from the activity history. + // Activities are returned newest-first by /api/staff/report, so the first + // non-empty history entry on each field is "most recent." + const submitterName = pickLatestText( + checkInSection?.fields.find((f) => f.descriptor.name === "Survey_completed_by"), + ); + const submitterEmail = pickLatestText( + checkInSection?.fields.find((f) => f.descriptor.name === "Survey_completed_by_email"), + ); + const stageLabel = data.currentStage ? data.options[STAGE_OPTION_GROUP_ID]?.find((o) => o.value === data.currentStage)?.label ?? data.currentStage @@ -149,6 +159,25 @@ export function StaffReportView({ /> {data.orgId}} /> + {(submitterName || submitterEmail) && ( +

+ + Most recent submitter + {" "} + {submitterName ?? "—"} + {submitterEmail && ( + <> + {" · "} + + {submitterEmail} + + + )} +

+ )}
@@ -251,13 +280,20 @@ function StaffSection({ const empty = section.fields.filter((f) => f.history.length === 0); const [showEmpty, setShowEmpty] = useState(false); - // Stage 5 Y1 matrix: pull Labor / Margin / Member_Sales quarterly fields - // out into a single tabular display matching the form's matrix layout. + // Stage 5 Y1 matrices: pull quarterly (Y1_Q*) and monthly (Y1_M*) field + // series out into compact tabular displays that mirror the form's matrix + // layout. Anything not consumed by a matrix falls through to the regular + // per-field list below. const isStage5 = section.groupName === "Stage_5"; - const matrixFields = isStage5 ? collectY1MatrixFields(filled) : null; + const quarterlyMatrix = isStage5 ? collectY1MatrixByPeriod(filled, "Q") : null; + const monthlyMatrix = isStage5 ? collectY1MatrixByPeriod(filled, "M") : null; + const matrixUsedNames = new Set([ + ...(quarterlyMatrix?.usedNames ?? []), + ...(monthlyMatrix?.usedNames ?? []), + ]); const filledOutsideMatrix = - matrixFields - ? filled.filter((f) => !matrixFields.usedNames.has(f.descriptor.name)) + matrixUsedNames.size > 0 + ? filled.filter((f) => !matrixUsedNames.has(f.descriptor.name)) : filled; return ( @@ -277,12 +313,11 @@ function StaffSection({ {filled.length} with data

- {matrixFields ? ( - + {quarterlyMatrix ? ( + + ) : null} + {monthlyMatrix ? ( + ) : null} {filledOutsideMatrix.length > 0 ? ( @@ -349,41 +384,58 @@ function CompactFieldRow({ const earlier = field.history.slice(1); return ( -
  • +
  • {field.descriptor.label} -
    - +
    + + + {latest.date ? ( - - · {formatShortDate(latest.date)} + + as of {formatShortDate(latest.date)} ) : null} + {earlier.length > 0 ? ( + + ) : null}
    - {earlier.length > 0 ? ( -
    - - {open ? ( -
      - {earlier.map((e, i) => ( -
    • - - {formatShortDate(e.date)} -
    • - ))} -
    - ) : null} -
    + {open && earlier.length > 0 ? ( +
      + {earlier.map((e, i) => ( +
    1. + + {formatShortDate(e.date)} + + + + +
    2. + ))} +
    ) : null}
  • ); @@ -427,6 +479,21 @@ function FieldValue({ ); } +/** + * Return the most recent non-empty string value from a field's history, or + * undefined if the field is missing or every entry is empty. Used to surface + * the latest submitter name / email at the top of the report. + */ +function pickLatestText(field: StaffReportField | undefined): string | undefined { + if (!field) return undefined; + for (const e of field.history) { + if (e.value === null || e.value === undefined) continue; + const s = String(e.value).trim(); + if (s) return s; + } + return undefined; +} + function fieldConfigFor(f: StaffReportField): FieldConfig { return { name: f.descriptor.name, @@ -461,41 +528,47 @@ function renderToFieldType(r: StaffReportField["descriptor"]["render"]): FieldCo } /** - * Stage 5 Y1 matrix: detect fields whose names match Y1_Q_ and - * group them into a read-only table mirroring the form's matrix layout. - * The metric set is whatever's actually present in the data so a Civi - * schema addition (e.g. Y1_Q*_Labor_Hours) appears automatically. + * Stage 5 Y1 matrix: detect fields whose names match Y1_

    _ for a + * given period letter (Q for quarterly, M for monthly) and group them into a + * read-only table mirroring the form's matrix layout. The metric set is + * whatever's actually present in the data, so a Civi schema addition (e.g. + * Y1_M*_Labor_Hours) appears automatically. */ interface Y1MatrixRow { metric: string; // e.g. "Labor" | "Margin" | "Member_Sales_" - label: string; // human label from the first field's descriptor (sans Y1_Q_ prefix) - byQuarter: Map; + label: string; // human label from the first field's descriptor (sans Y1_

    _ prefix) + byPeriod: Map; } interface Y1MatrixData { rows: Y1MatrixRow[]; - quarters: number[]; + periods: number[]; + periodLetter: "Q" | "M"; usedNames: Set; } -function collectY1MatrixFields(filled: StaffReportField[]): Y1MatrixData | null { - const re = /^Y1_Q(\d+)_(.+)$/; +function collectY1MatrixByPeriod( + filled: StaffReportField[], + periodLetter: "Q" | "M", +): Y1MatrixData | null { + const nameRe = new RegExp(`^Y1_${periodLetter}(\\d+)_(.+)$`); + const labelStripRe = new RegExp(`^Y1\\s*${periodLetter}\\d+\\s*`, "i"); const used = new Set(); const byMetric = new Map>(); - const quartersSet = new Set(); + const periodsSet = new Set(); const metricLabel = new Map(); for (const f of filled) { - const m = re.exec(f.descriptor.name); + const m = nameRe.exec(f.descriptor.name); if (!m) continue; - const quarter = Number(m[1]); + const period = Number(m[1]); const metric = m[2]; used.add(f.descriptor.name); - quartersSet.add(quarter); + periodsSet.add(period); if (!byMetric.has(metric)) byMetric.set(metric, new Map()); - byMetric.get(metric)!.set(quarter, f); + byMetric.get(metric)!.set(period, f); if (!metricLabel.has(metric)) { - // Strip "Y1 Q " prefix variants from the label if present. + // Strip the "Y1 Q " / "Y1 M " prefix from the label if present. const cleaned = f.descriptor.label - .replace(/^Y1\s*Q\d+\s*/i, "") + .replace(labelStripRe, "") .replace(/_/g, " ") .trim(); metricLabel.set(metric, cleaned || metric.replace(/_/g, " ")); @@ -503,35 +576,35 @@ function collectY1MatrixFields(filled: StaffReportField[]): Y1MatrixData | null } if (byMetric.size === 0) return null; - const quarters = Array.from(quartersSet).sort((a, b) => a - b); - const rows: Y1MatrixRow[] = Array.from(byMetric.entries()).map(([metric, byQuarter]) => ({ + const periods = Array.from(periodsSet).sort((a, b) => a - b); + const rows: Y1MatrixRow[] = Array.from(byMetric.entries()).map(([metric, byPeriod]) => ({ metric, label: metricLabel.get(metric) ?? metric, - byQuarter, + byPeriod, })); - return { rows, quarters, usedNames: used }; + return { rows, periods, periodLetter, usedNames: used }; } function Y1MatrixTable({ - rows, - quarters, + data, options, }: { - rows: Y1MatrixRow[]; - quarters: number[]; + data: Y1MatrixData; options: Record; }) { + const { rows, periods, periodLetter } = data; + const cadence = periodLetter === "Q" ? "quarterly" : "monthly"; return (

    - {quarters.map((q) => ( - + {periods.map((p) => ( + ))} @@ -539,11 +612,11 @@ function Y1MatrixTable({ {rows.map((row) => ( - {quarters.map((q) => { - const f = row.byQuarter.get(q); + {periods.map((p) => { + const f = row.byPeriod.get(p); const latest = f?.history[0]; return ( -
    - Year 1 quarterly · latest values + Year 1 {cadence} · latest values
    MetricQ{q}{periodLetter}{p}
    {row.label} + {f && latest ? (