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