// Project Operations — cohort overview first, drill into a single project on click. // // Two instruments, two cadences: // • Team Health Survey (27 items) — taken TWICE per project: Baseline (kickoff) + // Post-Health (~6 months in). Stages: Baseline pending → Baseline complete → // Post-Health window open → Post-Health complete. // • Meeting Effectiveness Survey (12 items) — taken WEEKLY. This is the ongoing // pulse the PM watches. // // PM's #1 question on this page: "Are the surveys being filled?" // We answer that with: Team Health stage status per project + Meeting Effectiveness // weekly response rate. // Stage helper — derived from team.preDone / team.postDone + project age // Team Health is a 2-wave instrument: Baseline at kickoff, Post-Health ~6 months later. // Stage progression depends on what's submitted AND how long the project has been in the field. // - baseline-pending: kickoff survey not submitted yet // - baseline-complete: kickoff submitted; <6 months in, Post-Health window not open yet // - post-window: ≥6 months in, Post-Health not yet submitted (action needed) // - complete: both waves submitted const POST_HEALTH_WEEKS = 26; // ~6 months // Progress color: <50% red, 50-79% yellow, >=80% green function progressColor(rate) { if (rate >= 0.80) return riskColor("healthy"); if (rate >= 0.50) return riskColor("watch"); return riskColor("critical"); } function teamHealthStage(team) { if (team.postDone) return { id: "complete", label: "Post-Health complete", short: "Complete" }; // age = how many weeks since this project's own kickoff (different per project) const age = team.weekInProject ?? team.weekly.length; const postWindowOpen = age >= POST_HEALTH_WEEKS; if (team.preDone && postWindowOpen) return { id: "post-window", label: "Post-Health window open", short: "Post-Health window" }; if (team.preDone) return { id: "baseline-complete", label: "Baseline complete", short: "Baseline complete" }; return { id: "baseline-pending", label: "Baseline pending", short: "Baseline pending" }; } // stage colors — kept restrained, distinct from response-rate red/amber/green function stageColor(id) { if (id === "complete") return "oklch(0.5 0.10 200)"; // muted blue — done if (id === "post-window") return "oklch(0.55 0.13 70)"; // amber — action needed if (id === "baseline-complete") return "oklch(0.5 0.07 145)"; // muted green — in field return "oklch(0.55 0.005 250)"; // neutral — pending } const { useState: useStatePM, useMemo: useMemoPM } = React; // ---------- Date helpers (plain-English week labels) ---------- function addDays(iso, days) { const d = new Date(iso + "T00:00:00"); d.setDate(d.getDate() + days); return d; } function fmtMD(d) { return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } function fmtMDY(d) { return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); } function weekRange(studyStart, weekNum) { const start = addDays(studyStart, (weekNum - 1) * 7); const end = addDays(studyStart, (weekNum - 1) * 7 + 6); const now = new Date(); now.setHours(0, 0, 0, 0); const cap = end > now ? now : end; return { start, end: cap, rawEnd: end, label: `${fmtMD(start)} – ${fmtMD(cap)}`, shortLabel: fmtMD(start) }; } function weekRelativeLabel(weekNum, currentWeek) { if (weekNum === currentWeek) return "This week"; if (weekNum === currentWeek - 1) return "Last week"; if (weekNum === currentWeek + 1) return "Next week"; const diff = currentWeek - weekNum; if (diff > 0) return `${diff} weeks ago`; return `In ${-diff} weeks`; } // ---------- Subtle construction motif (used very sparingly) ---------- function HardHatMark({ size = 14, color = "currentColor", className = "" }) { // Minimalist hard-hat silhouette — used as a tiny eyebrow icon, not decoration. return ( ); } // ============================================================ // MAIN ROUTER // ============================================================ function PMView({ data }) { const { teams } = data; const [selectedId, setSelectedId] = useStatePM(null); // null = cohort overview // currentWeek = last week in the array (from weeksElapsed in backend) const latestWeek = teams[0].weekly[teams[0].weekly.length - 1].week; // Default selected week to the most recent week with any cohort responses const mostRecentActiveWeek = (() => { for (let w = latestWeek; w >= 1; w--) { const total = teams.reduce((s, t) => s + ((t.weekly.find(x => x.week === w) || {}).responses || 0), 0); if (total > 0) return w; } return latestWeek; })(); const [week, setWeek] = useStatePM(mostRecentActiveWeek); const [toast, setToast] = useStatePM(null); React.useEffect(() => { const handler = (e) => { setToast(e.detail); const t = setTimeout(() => setToast(null), 2200); return () => clearTimeout(t); }; window.addEventListener("pm-toast", handler); return () => window.removeEventListener("pm-toast", handler); }, []); if (selectedId) { const team = teams.find(t => t.id === selectedId); return ( <> setSelectedId(null)}/> ); } return ( <> ); } // Global helper for Copy/Send actions — dispatches toast via window event function pmCopyLink(label, msg = "Survey link copied") { const url = `https://survey.r-o.com/${label || "team"}`; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url).catch(() => {}); } window.dispatchEvent(new CustomEvent("pm-toast", { detail: msg })); } function pmSendReminder(msg = "Reminder sent to team") { window.dispatchEvent(new CustomEvent("pm-toast", { detail: msg })); } function pmDownload(msg = "QR poster downloaded") { window.dispatchEvent(new CustomEvent("pm-toast", { detail: msg })); } // Tiny floating toast for confirming Copy / Send actions function Toast({ msg }) { if (!msg) return null; return (
{msg}
); } // ============================================================ // COHORT OVERVIEW — landing screen // ============================================================ // Period picker — Entire study + one option per week (most recent first) function PMDateRangeScope({ value, onChange, currentWeek, studyStart }) { const opts = [ { v: "all", label: "Entire study", sub: `Wk 1 – Wk ${currentWeek} · ${currentWeek} week${currentWeek > 1 ? "s" : ""}` }, ...Array.from({length: currentWeek}, (_, i) => { const wk = currentWeek - i; const r = weekRange(studyStart, wk); return { v: wk, label: `Week ${wk}`, sub: r.label + (wk === currentWeek ? " · current" : "") }; }), ]; const [open, setOpen] = useStatePM(false); const cur = opts.find(o => o.v === value) || opts[0]; const INK_C = "oklch(0.22 0.02 250)"; const SUBINK_C = "oklch(0.45 0.015 250)"; const RULE_C = "oklch(0.88 0.005 250)"; const PAPER2_C = "oklch(0.97 0.006 80)"; return (
{open && ( <>
setOpen(false)}/>
Select period
{opts.map(o => ( ))}
)}
); } function PMCohortOverview({ teams, studyStart, currentWeek, week, setWeek, onPickProject }) { const totalPeople = teams.reduce((s, t) => s + t.size, 0); // Period scope — "all" (entire study) or a specific week number const [scope, setScope] = useStatePM("all"); const scopeWeeks = useMemoPM(() => { if (scope === "all") return Array.from({length: currentWeek}, (_, i) => i + 1); return [scope]; // scope is a week number }, [scope, currentWeek]); // Sync sparkline selection when a specific week is picked React.useEffect(() => { if (typeof scope === "number") setWeek(scope); }, [scope]); const scopeStartWk = scopeWeeks[0]; const scopeEndWk = scopeWeeks[scopeWeeks.length - 1]; const scopeStartDate = weekRange(studyStart, scopeStartWk).start; const scopeEndDate = weekRange(studyStart, scopeEndWk).end; // Meeting Effectiveness — averaged across scoped weeks const scopedResponses = teams.reduce((s, t) => s + scopeWeeks.reduce((w, wk) => w + (t.weekly.find(x => x.week === wk)?.responses || 0), 0), 0); const scopedDenom = totalPeople; // per-week denominator const scopedRate = scopedDenom && scopeWeeks.length ? scopedResponses / (totalPeople * scopeWeeks.length) : 0; // Comparison: prior equivalent window const priorWeeks = scopeWeeks.map(w => w - scopeWeeks.length).filter(w => w >= 1); const priorResponses = teams.reduce((s, t) => s + priorWeeks.reduce((w, wk) => w + (t.weekly.find(x => x.week === wk)?.responses || 0), 0), 0); const priorDenom = totalPeople; // per-week denominator const priorRate = priorDenom && priorWeeks.length ? priorResponses / (totalPeople * priorWeeks.length) : 0; const isCurrent = week === currentWeek; const range = weekRange(studyStart, week); // Team Health — 2-wave stages const stageBuckets = teams.reduce((acc, t) => { const id = teamHealthStage(t).id; acc[id] = (acc[id] || 0) + 1; return acc; }, {}); const baselinePending = stageBuckets["baseline-pending"] || 0; const baselineComplete = stageBuckets["baseline-complete"] || 0; const postWindow = stageBuckets["post-window"] || 0; const completeCount = stageBuckets["complete"] || 0; // Counts of completed Baseline / Post-Health surveys submitted across the cohort const baselineDoneCount = teams.filter(t => t.preDone).length; const postDoneCount = teams.filter(t => t.postDone).length; // People-level totals (denominator: every individual in the study) const baselinePeopleSubmitted = teams.reduce((s, t) => s + (t.baselineSubmitted || 0), 0); const postHealthPeopleSubmitted = teams.reduce((s, t) => s + (t.postHealthSubmitted || 0), 0); // Next Post-Health window opening date across the cohort const nextPostOpens = teams .filter(t => !t.postDone) .map(t => t.postHealthOpenDate) .sort()[0]; return (
{/* Hero */}

{teams.length} active projects

{scope === "all" ? `${fmtMD(scopeStartDate)} – ${fmtMDY(scopeEndDate)}` : weekRange(studyStart, scope).label}
{/* At-a-glance counts — directly under the headline */}
s + t.weekly.reduce((a, w) => a + w.responses, 0), 0).toLocaleString()} hint="study to date"/>
{/* MEETING EFFECTIVENESS — primary tracker, full-width */}

Meeting Effectiveness · {scope === "all" ? "entire study" : `Week ${scope}`}

12 questions · weekly · {scope === "all" ? `Wk 1–${scopeEndWk}` : weekRange(studyStart, scope).label}

{Math.round(scopedRate * 100)}%
{scope !== "all" ? <>{scopedResponses} of {totalPeople} responded : <>{scopedResponses} submissions · {totalPeople} people}
{scope !== "all" ? (priorWeeks.length ? `Prior: Wk ${priorWeeks[0]} was ${Math.round(priorRate * 100)}% · ${priorResponses} submissions` : "No prior week data") : `avg ${Math.round(scopedRate * 100)}%/wk · ${scopeWeeks.length} week${scopeWeeks.length > 1 ? "s" : ""}`}
{ setWeek(w); setScope(w); }} highlightWeeks={scopeWeeks}/>
{/* TEAM HEALTH — Pre→Post 2-bullet timeline */}
{/* Projects grid */}

Projects · {teams.length}

Each card shows the Team Health stage and the Meeting Effectiveness response rate for{" "} {scope === "all" ? `the entire study (Wk 1\u2013${currentWeek}, ${currentWeek} week${currentWeek > 1 ? "s" : ""})` : scope === currentWeek ? "this week" : `Wk ${scope}`} . Open a project for QR codes and outstanding people.

Meeting response
{teams.map(t => ( onPickProject(t.id)}/> ))}
{/* Response activity log */}

Response activity

Submissions in the selected range — {fmtMD(scopeStartDate)} – {fmtMDY(scopeEndDate)}. Anonymous — we never see who submitted.

{scope === "all" ? "All study" : `Week ${scope}`}
{/* Footer note */}

Every survey is anonymous. We only see counts and team-level summaries — never an individual's answers. Health scores and qualitative findings are reviewed by the TAMU research team and shared back at study milestones.

); } // ---------- Team Health cohort timeline ---------- // Two data points per project: Baseline (kickoff) and Post-Health (~6mo in). // Renders one row per project with two milestones — done dot vs open ring, // connected by a horizontal time rail. People-level submission count on the right. function TeamHealthCohortTimeline({ teams, totalPeople, baselinePeopleSubmitted, postHealthPeopleSubmitted, nextPostOpens }) { const fmt = (iso) => { if (!iso) return ""; const d = new Date(iso + "T00:00:00"); return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); }; const fmtLong = (iso) => { if (!iso) return ""; const d = new Date(iso + "T00:00:00"); return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); }; // Sort: baseline-pending first, then by kickoff date const ordered = [...teams].sort((a, b) => { if (a.preDone !== b.preDone) return a.preDone ? 1 : -1; return a.kickoffDate.localeCompare(b.kickoffDate); }); const baselineDone = teams.filter(t => t.preDone).length; const postDone = teams.filter(t => t.postDone).length; const [expanded, setExpanded] = useStatePM(false); const baselinePct = totalPeople ? Math.round((baselinePeopleSubmitted / totalPeople) * 100) : 0; const postPct = totalPeople ? Math.round((postHealthPeopleSubmitted / totalPeople) * 100) : 0; return (
{/* HEADER — always visible (expansion no longer needed; detail is always open) */}

Team Health · Pre → Post

{teams.length} projects · two waves each

{/* Baseline progress bar only */}
Baseline
{baselinePeopleSubmitted} / {totalPeople}
{baselineDone} of {teams.length} projects complete
{/* Per-project status pip strip — at-a-glance health of all 11 */}
{ordered.map(t => ( ))}
{/* DETAIL — full per-project timeline (always shown) */}
{/* Header rail */}
Project
Baseline
Submissions
    {ordered.map(t => { return (
  • {t.short}
    {t.code} · {t.size} ppl
    {t.preDone ? "Submitted" : "Pending"}
    {t.baselineSubmitted} / {t.size}
    people
  • ); })}
); } // Milestone dot: filled = submitted, outlined = pending/upcoming function MilestoneDot({ done, size = 12, pending = false }) { const s = `${size}px`; if (done) { return ; } return ( ); } // ---------- Response activity log (PM view) ---------- // Operations-friendly: shows who submitted what & when, anonymized. // No scores, no durations — just project / instrument / wave / time / status. function PMResponseLog({ teams, scope = "1w", scopeWeeks = [], studyStart }) { const all = []; const today = new Date(); const studyStartMs = new Date((studyStart || "2026-04-26") + "T00:00:00").getTime(); teams.forEach((t, ti) => { (t.recentSubmissions || []).forEach((s, i) => { const submitted = new Date(s.date); const minsAgo = Math.round((today.getTime() - submitted.getTime()) / 60000); // Compute the study week number from the actual submission date const daysDiff = Math.floor((submitted.getTime() - studyStartMs) / (24 * 60 * 60 * 1000)); const wkNum = daysDiff >= 0 ? Math.max(1, Math.floor(daysDiff / 7) + 1) : 0; all.push({ id: `${t.id}-${s.type}-${i}`, team: t, instrument: s.type === "health" ? "Team Health" : "Meeting Effectiveness", wave: s.type === "health" ? "Baseline" : `Week ${wkNum}`, wkNum, submitted, minsAgo, complete: true, }); }); }); // Filter by scope: use week-number matching, not elapsed minutes const filtered = all.filter(r => { if (scope === "all") return true; return scopeWeeks.includes(r.wkNum); }); filtered.sort((a, b) => b.submitted - a.submitted); const rowCap = scope === "all" ? 30 : 14; const rows = filtered.slice(0, rowCap); const fmtAgo = (m) => { if (m < 60) return `${m}m ago`; if (m < 60 * 24) return `${Math.floor(m/60)}h ${m%60}m ago`; return `${Math.floor(m/1440)}d ago`; }; const fmtTime = (d) => d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }); const fmtDate = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); return (
Project
Instrument
Wave
Submitted
Status
{rows.map(r => (
{r.team.short} {r.team.code}
{r.instrument}
{r.wave}
{r.minsAgo < 60 * 24 ? `${fmtTime(r.submitted)} · ${fmtAgo(r.minsAgo)}` : `${fmtDate(r.submitted)} · ${fmtAgo(r.minsAgo)}`}
{r.complete ? ( Complete ) : ( Partial )}
))}
Showing latest {rows.length} · all submissions are anonymous.
); } function TrendChip({ trend }) { const pts = Math.round(trend * 100); if (Math.abs(pts) < 1) { return (
Steady vs last week
); } const up = pts > 0; const color = up ? "oklch(0.42 0.12 145)" : "oklch(0.5 0.14 28)"; const bg = up ? "oklch(0.96 0.04 145)" : "oklch(0.97 0.03 28)"; return (
{up ? : } {up ? "+" : ""}{pts} pts vs last week
); } // Compact stage pill — used in the cohort slim status strip function StagePill({ count, label, color }) { return (
{count} {label}
); } // Stage funnel cell — used in the cohort Team Health card function StageCell({ count, total, label, hint, color, last }) { const pct = total ? Math.round((count / total) * 100) : 0; return (
{/* color rail */}
{count}of {total}
{label}
{hint}
); } // Bottom strip stat (cohort hero footer) function BottomStat({ label, value, hint }) { return (
{label}
{value}
{hint &&
{hint}
}
); } function StatusRow({ count, total, label, hint, color, last }) { return (
{label}
{hint}
{count} / {total}
); } function LegendDot({ color, label }) { return ( {label} ); } // 8-week cohort response-rate sparkline — clickable: each week is selectable function CohortSparkline({ teams, totalPeople, currentWeek, studyStart, selected, onPick, highlightWeeks = [] }) { const hi = new Set(highlightWeeks); const weeks = teams[0].weekly.map(w => { const responses = teams.reduce((s, t) => s + (t.weekly.find(x => x.week === w.week)?.responses || 0), 0); return { week: w.week, rate: totalPeople ? responses / totalPeople : 0, responses }; }); const width = 760, height = 160; const padL = 36, padR = 24, padT = 22, padB = 36; const w = width - padL - padR, h = height - padT - padB; const xs = i => weeks.length <= 1 ? padL + w / 2 : padL + (i / (weeks.length - 1)) * w; const ys = v => padT + h - v * h; const path = weeks.map((d, i) => `${i ? "L" : "M"}${xs(i)},${ys(d.rate)}`).join(" "); const area = path + ` L${xs(weeks.length - 1)},${padT + h} L${xs(0)},${padT + h} Z`; return (
Weekly response rate · click any week
{/* gridline at 100% */} 100% {hi.size > 0 && hi.size < weeks.length && (() => { const idxs = weeks.map((d, i) => hi.has(d.week) ? i : -1).filter(i => i >= 0); if (idxs.length === 0) return null; const x0 = xs(idxs[0]) - 10; const x1 = xs(idxs[idxs.length - 1]) + 10; return ; })()} {weeks.map((d, i) => { const isSel = d.week === selected; const isCur = d.week === currentWeek; const showLabel = isSel || isCur; return ( onPick && onPick(d.week)}> {/* hit area */} {isSel && } {showLabel && ( {Math.round(d.rate * 100)}% )} Wk {d.week} {isCur && ( NOW )} ); })}
); } // Compact dropdown picker for the hero — calendar-style list of weeks function CohortWeekPicker({ teams, studyStart, currentWeek, selected, onPick }) { const [open, setOpen] = useStatePM(false); const weeks = teams[0].weekly; const totalPeople = teams.reduce((s, t) => s + t.size, 0); const range = weekRange(studyStart, selected); const isCurrent = selected === currentWeek; return (
{open && ( <>
setOpen(false)}/>
Pick a week
{weeks.slice().reverse().map(w => { const r = weekRange(studyStart, w.week); const responses = teams.reduce((s, t) => s + (t.weekly.find(x => x.week === w.week)?.responses || 0), 0); const rate = totalPeople ? responses / totalPeople : 0; const sel = w.week === selected; const cur = w.week === currentWeek; const c = progressColor(rate); return ( ); })}
)}
); } // Project card — updates with selected scope (specific week or entire study) function ProjectCard({ team, currentWeek, scopeWeeks, onClick }) { const isAll = !scopeWeeks || scopeWeeks.length > 1; let pct, responses, denom, diff, scopeLabel, footerText; if (isAll) { // Multi-week / entire study: total responses ÷ (team size × number of weeks in scope) const weeks = scopeWeeks ? scopeWeeks : Array.from({length: currentWeek}, (_, i) => i + 1); const totalResp = weeks.reduce((s, wk) => s + (team.weekly.find(w => w.week === wk)?.responses || 0), 0); denom = team.size * weeks.length; const avgRate = denom ? totalResp / denom : 0; pct = Math.round(avgRate * 100); responses = totalResp; diff = null; scopeLabel = `${weeks.length}-wk avg`; footerText = `${responses} of ${denom} possible (${team.size} ppl × ${weeks.length} wk${weeks.length > 1 ? "s" : ""})`; } else { // Single week const wkNum = scopeWeeks[0]; const entry = team.weekly.find(w => w.week === wkNum) || { responses: 0, rate: 0, week: wkNum }; const prevEntry = wkNum > 1 ? team.weekly.find(w => w.week === wkNum - 1) : null; pct = Math.round(entry.rate * 100); responses = entry.responses; denom = team.size; const prevPct = prevEntry ? Math.round(prevEntry.rate * 100) : null; diff = prevPct !== null ? pct - prevPct : null; scopeLabel = wkNum === currentWeek ? "this week" : wkNum === currentWeek - 1 ? "last week" : `Wk ${wkNum}`; const pending = denom - responses; footerText = responses === denom ? "Everyone responded" : wkNum < currentWeek ? `${responses} of ${denom} responded` : `${pending} pending`; } const status = pct >= 80 ? "healthy" : pct >= 50 ? "watch" : "critical"; const accent = riskColor(status); const stage = teamHealthStage(team); const stageC = stageColor(stage.id); return ( ); } // ============================================================ // PROJECT DETAIL — drill-in // ============================================================ function PMProjectDetail({ team, studyStart, currentWeek, week: parentWeek, setWeek: parentSetWeek, onBack }) { const [localWeek, setLocalWeek] = useStatePM(parentWeek ?? currentWeek); const week = parentWeek ?? localWeek; const setWeek = parentSetWeek ?? setLocalWeek; const wk = team.weekly.find(w => w.week === week) || team.weekly[team.weekly.length - 1]; const lastWk = team.weekly.find(w => w.week === week - 1); const pct = Math.round(wk.rate * 100); const pending = team.size - wk.responses; const status = wk.rate >= 0.80 ? "healthy" : wk.rate >= 0.50 ? "watch" : "critical"; const range = weekRange(studyStart, week); const relLabel = weekRelativeLabel(week, currentWeek); return (
{/* Back */} {/* Project header */}
{team.code} · {team.region} · {team.sector}

{team.name}

{team.size} team members · {team.phase}

{/* HERO — QR codes + Team Health stage panel */}
{/* QR hero — 7 cols */}
Survey access

QR codes for this project

Print on a sign, post in the trailer, or hand out at the next huddle. Scans open Qualtrics.

{/* Team Health stage — 5 cols */}
{/* Meeting Effectiveness — weekly response history */}

Meeting Effectiveness · weekly response history

Click any week to see how many of the {team.size} team members submitted that week's pulse.

{pct} % this week
w.rate))*100)}%`}/> s+w.responses,0).toLocaleString()}/> w.rate))*100)}%`} hint={(() => { const best = team.weekly.reduce((a,b)=>b.rate>a.rate?b:a); return weekRange(studyStart, best.week).label; })()}/>
{/* What to do next */}

What to do next

Suggestions based on Team Health stage and Meeting Effectiveness response trend.

{/* Privacy footer */}

Survey responses are anonymous. We only see counts and team-level averages — never who said what. Health scores and qualitative findings are reviewed by the TAMU research team and shared back at study milestones.

); } // Team Health stage panel — right column on the project detail. // Shows: current stage, Baseline progress, Post-Health window, what to share via QR. function TeamHealthStagePanel({ team }) { const stage = teamHealthStage(team); const stageC = stageColor(stage.id); // Use real submission counts from backend const baselineSubmitted = team.baselineSubmitted || 0; const postSubmitted = team.postHealthSubmitted || 0; const stages = [ { id: "baseline-pending", label: "Baseline pending" }, { id: "baseline-complete", label: "Baseline complete" }, { id: "post-window", label: "Post-Health window" }, { id: "complete", label: "Complete" }, ]; // current step index: post-window also implies baseline complete const currentIdx = stage.id === "complete" ? 3 : stage.id === "post-window" ? 2 : (team.preDone ? 1 : 0); return (
Team Health · stage
{stage.label}
{/* 4-step progress strip */}
{stages.map((s, i) => { const done = i <= currentIdx; return (
{s.label}
); })}
{/* Baseline / Post-Health submission rows */}
{/* What to do */}
{stage.id === "baseline-pending" && ( <>Share the Team Health QR at the next huddle. Goal: every team member submits the kickoff survey within the first two weeks. )} {stage.id === "post-window" && ( <>The 6-month window has opened. Re-share the Team Health QR with your team to capture the Post-Health wave. )} {stage.id === "complete" && ( <>Both Team Health waves are submitted. Continue running Meeting Effectiveness weekly until the project closes. )}
); } function SurveyWaveRow({ waveLabel, sublabel, done, locked, submitted, total, color }) { const pct = total ? Math.round((submitted / total) * 100) : 0; return (
{waveLabel}
{sublabel}
{locked ? ( Opens after Baseline ) : ( <>
{submitted}/{total}
{done ? "Submitted" : `${pct}% submitted`}
)}
{!locked && (
)}
); } function QRHero({ slug, kind, label, sub }) { return (
{label}
{sub}
{`${label}
); } // Response history chart — 8 weeks, plain English labels function ResponseHistoryChart({ team, selected, onPick, studyStart, currentWeek }) { const padL = 36, padR = 16, padT = 28, padB = 68; const slot = 110; const width = padL + padR + slot * team.weekly.length; const height = 240; const w = width - padL - padR, h = height - padT - padB; const barW = Math.min(slot - 22, 56); return (
{/* gridlines: 0%, 50%, 100% */} {[0, 0.5, 1].map(p => ( {Math.round(p * 100)}% ))} {team.weekly.map((d, i) => { const cx = padL + i * slot + slot / 2; const bh = d.rate * h; const y = padT + h - bh; const sel = selected === d.week; const isCurrent = d.week === currentWeek; const c = progressColor(d.rate); const range = weekRange(studyStart, d.week); const isPast = d.week < currentWeek; const showRel = sel || isCurrent; return ( onPick && onPick(d.week)}> {sel && } {Math.round(d.rate * 100)}% Wk {d.week} {`${d.responses}/${team.size}`} ); })}
); } function MiniStat({ label, value, hint }) { return (
{label}
{value}
{hint &&
{hint}
}
); } // Prioritized action cards — covers BOTH instruments: // Team Health (Baseline + Post-Health, 2 waves) and Meeting Effectiveness (weekly). // No "send reminder" buttons — the PM's lever is sharing the QR code at huddles. function PMSimpleNotes({ team, currentWeek, studyStart }) { const wk = team.weekly.find(w => w.week === currentWeek) || team.weekly[team.weekly.length - 1]; const lastWk = team.weekly.find(w => w.week === currentWeek - 1); const trend = lastWk ? wk.rate - lastWk.rate : 0; const allWeeksTrend = team.weekly[team.weekly.length - 1].rate - team.weekly[0].rate; const avgRate = avg(team.weekly.map(w => w.rate)); const stage = teamHealthStage(team); const items = []; // ─── Team Health (2-wave) priorities ───────────────────────────────────── if (stage.id === "baseline-pending") { items.push({ kind: "alert", title: "Baseline survey hasn't been completed yet", body: `Team Health runs only twice per project — Baseline at kickoff and Post-Health ~6 months later. Share the Team Health QR with the team this week so we have a starting point for the study.`, }); } else if (stage.id === "post-window") { items.push({ kind: "neutral", title: "Post-Health window is open", body: `It's been ~6 months since kickoff. Re-share the Team Health QR with the team to capture the second wave. Once submitted, Team Health is complete for this project.`, }); } else if (stage.id === "complete") { items.push({ kind: "good", title: "Both Team Health waves are complete", body: `Baseline and Post-Health are both submitted. The TAMU research team will share the comparison findings at the next milestone review.`, }); } // ─── Meeting Effectiveness (weekly) priorities ────────────────────────── if (wk.rate < 0.9) { items.push({ kind: "alert", title: `Meeting Effectiveness response is ${Math.round(wk.rate * 100)}% this week`, body: `${wk.responses} of ${team.size} submitted this week's pulse. Posting the Meeting Effectiveness QR at the next huddle usually closes the gap.`, }); } else if (trend < -0.15) { items.push({ kind: "alert", title: `Meeting response dropped ${Math.abs(Math.round(trend * 100))} points week-over-week`, body: `Down from ${Math.round(lastWk.rate * 100)}% last week to ${Math.round(wk.rate * 100)}% this week. Worth a quick check-in to see if anything's changed for the team.`, }); } // Trend insights — Meeting Effectiveness if (trend > 0.1) { items.push({ kind: "good", title: `Meeting response up ${Math.round(trend * 100)} points week-over-week`, body: `Whatever you did, keep it going.`, }); } if (allWeeksTrend > 0.15) { items.push({ kind: "good", title: "Meeting Effectiveness participation has steadily improved", body: `Up ${Math.round(allWeeksTrend * 100)} points since week 1. The team is building the weekly habit.`, }); } if (allWeeksTrend < -0.15 && wk.rate >= 0.9) { items.push({ kind: "alert", title: "Meeting response trending down across the study", body: `Down ${Math.abs(Math.round(allWeeksTrend * 100))} points since week 1. Consider a team huddle on why.`, }); } if (wk.responses === team.size) { items.push({ kind: "good", title: "Everyone submitted this week's Meeting pulse", body: `Full weekly participation — that's the bar. This week's data will be most reliable.`, }); } // Fallback if (items.length === 0) { items.push({ kind: "good", title: "On track on both surveys", body: `Meeting Effectiveness averaging ${Math.round(avgRate * 100)}% across the study, and Team Health is ${stage.label.toLowerCase()}.`, }); } return (
{items.map((it, i) => ( ))}
); } function ActionCard({ kind, title, body, action, onAction }) { const palette = kind === "good" ? { bar: riskColor("healthy"), bg: "oklch(0.97 0.025 145)", icon: "✓" } : kind === "alert" ? { bar: riskColor("critical"), bg: "oklch(0.98 0.018 28)", icon: "!" } : { bar: SUBINK, bg: PAPER_2, icon: "→" }; return (
{palette.icon}
{title}
{body}
{action && ( )}
); } Object.assign(window, { PMView });