// Researcher analytics workspace — TAMU research team only. // Two sub-views: (1) Team Health analysis (2) Meeting Effectiveness analysis // Both support drill-in on a single project from the matrix. const { useState: useStateRC, useMemo: useMemoRC } = React; // ----- Open-ended response pool (demo data) ----- const OPEN_RESPONSES_HEALTH = [ { theme: "Trust & Belonging", sentiment: 1, q: "What's going well on this team?", text: "People speak up early when something feels off. Last week a young engineer flagged a sequencing issue and it was treated like a gift, not a complaint." }, { theme: "Adaptive Execution", sentiment: -1, q: "What's getting in the way?", text: "RFI turnaround. We are blocked on three submittals and the design team has not closed the loop in over two weeks." }, { theme: "Energy & Well-Being", sentiment: 1, q: "What's energizing the team?", text: "Pre-task planning is sharp on this job. Crews show up knowing what's expected and we end the day on time." }, { theme: "Alignment & Ownership", sentiment: -1, q: "What's getting in the way?", text: "We have two PMs giving slightly different answers on schedule. It's small but it costs us trust with the subs." }, { theme: "Capability & Growth", sentiment: 1, q: "What's working?", text: "I feel like I'm being stretched in the right way. The senior super lets me try things and walks me through what I missed afterward." }, { theme: "Trust & Belonging", sentiment: -1, q: "What's getting in the way?", text: "Late changes from the owner are bleeding the energy out of the foreman. Morale is flat in the Monday huddle." }, { theme: "Adaptive Execution", sentiment: 1, q: "What's working?", text: "Decision logs are paying off. New folks can catch up in 10 minutes instead of pulling someone aside for an hour." }, { theme: "Overall", sentiment: 1, q: "Would you work with this team again?", text: "Yes. The standard is high but the support is real. That's the bar I want for every job." }, { theme: "Energy & Well-Being", sentiment: -1, q: "What's getting in the way?", text: "Saturdays are getting normalized again. We talked about it but nothing has changed in three weeks." }, ]; const OPEN_RESPONSES_MEETING = [ { sentiment: 1, q: "What worked in this meeting?", text: "We left with a list of three things to do and who owns each. That has not always been the case." }, { sentiment: -1, q: "What could be better?", text: "Half the agenda items did not need everyone in the room. Subs sat through 40 minutes that did not concern them." }, { sentiment: 1, q: "What worked?", text: "The owner heard the schedule risk for the first time and asked good questions instead of pushing back." }, { sentiment: -1, q: "What could be better?", text: "We re-litigated decisions from last week. If we trust the decision log we don't need to keep re-opening them." }, { sentiment: 0, q: "Anything to flag?", text: "Audio in the trailer cut out for the remote attendees. Twice. We need to fix that before the OAC on Thursday." }, { sentiment: 1, q: "What worked?", text: "Short, focused, ended early. People had time to actually go execute on what we decided." }, ]; // ============================================================ // MAIN RESEARCHER VIEW — sub-tab switcher + drill-in router // ============================================================ // Scope a team object to a window of weeks. Returns a derived team // with weekly[] trimmed and current/trend/responseRate recomputed. function scopeTeam(team, range, latestWeek) { if (range === "all") return team; const wks = range === "4w" ? 4 : range === "2w" ? 2 : range === "1w" ? 1 : 99; const minWeek = Math.max(1, latestWeek - wks + 1); const weekly = team.weekly.filter(w => w.week >= minWeek); if (!weekly.length) return team; const last = weekly[weekly.length - 1]; const first = weekly[0]; const responses = weekly.reduce((s,w)=>s+w.responses, 0); // Scope-aware denominator (matches pm.jsx project-card math): each week of the // window adds team.size potential responses, e.g. 14-person team over 2 weeks => 28. const denom = team.size * weekly.length; return { ...team, weekly, current: last.score, trend: (last.score != null && first.score != null) ? last.score - first.score : 0, responseRate: denom ? responses / denom : team.responseRate, }; } function ResearcherView({ data }) { const { teams } = data; const [tab, setTab] = useStateRC("health"); // "health" | "meeting" const [drillId, setDrillId] = useStateRC(null); const [scope, setScope] = useStateRC("all"); // all | 4w | 2w | 1w const latestWeek = data.cohortWeekly[data.cohortWeekly.length - 1].week; const scopedData = useMemoRC(() => { if (scope === "all") return data; const wks = scope === "4w" ? 4 : scope === "2w" ? 2 : 1; const minWeek = Math.max(1, latestWeek - wks + 1); const cohortWeekly = data.cohortWeekly.filter(w => w.week >= minWeek); const teamsScoped = data.teams.map(t => scopeTeam(t, scope, latestWeek)); // Recompute summary.avgHealth from scoped current values const summary = { ...data.summary, avgHealth: r1(avg(teamsScoped.map(t => t.current))), weeklyResponsesThisWeek: cohortWeekly.reduce((s,w)=>s+w.responses, 0), }; return { ...data, teams: teamsScoped, cohortWeekly, summary }; }, [data, scope, latestWeek]); if (drillId) { // Drill-in always shows full team history regardless of scope const team = data.teams.find(t => t.id === drillId); return tab === "health" ? setDrillId(null)}/> : setDrillId(null)}/>; } return (
TAMU · Research workspace

Analysis

{teams.length} R-O project teams · Week {data.summary.weeksElapsed} of {data.summary.weeksTotal}

N = {scopedData.summary.totalResponses} α = 0.88
{/* Sub-tab switcher */}
setTab("health")} label="Team Health" sub="Pre / Post"/> setTab("meeting")} label="Meeting Effectiveness" sub="Weekly · per-meeting"/>
{tab === "health" ? : }
); } // Date range scope picker — 4 presets that scope cohort views function DateRangeScope({ value, onChange, latestWeek, studyStart }) { const opts = [ { v: "all", label: "All study", sub: `Wk 1–${latestWeek}` }, { v: "4w", label: "Last 4 weeks", sub: `Wk ${Math.max(1, latestWeek - 3)}–${latestWeek}` }, { v: "2w", label: "Last 2 weeks", sub: `Wk ${Math.max(1, latestWeek - 1)}–${latestWeek}` }, { v: "1w", label: "This week", sub: `Wk ${latestWeek}` }, ]; const [open, setOpen] = useStateRC(false); const cur = opts.find(o => o.v === value); return (
{open && ( <>
setOpen(false)}/>
Scope analysis to
{opts.map(o => ( ))}
)}
); } function SubTab({ active, onClick, label, sub }) { return ( ); } // ============================================================ // TEAM HEALTH ANALYSIS // ============================================================ function HealthAnalysis({ data, onSelectTeam }) { const { teams, dimensions, summary, cohortWeekly, cohortDimensions } = data; const [riskFilter, setRiskFilter] = useStateRC("all"); const [sortBy, setSortBy] = useStateRC("health-asc"); const filtered = useMemoRC(() => { let arr = [...teams]; if (riskFilter !== "all") arr = arr.filter((t) => t.risk === riskFilter); arr.sort((a, b) => { if (sortBy === "health-asc") return a.current - b.current; if (sortBy === "health-desc") return b.current - a.current; if (sortBy === "trend") return a.trend - b.trend; if (sortBy === "response") return a.responseRate - b.responseRate; return a.name.localeCompare(b.name); }); return arr; }, [teams, sortBy, riskFilter]); // Pre / Post for cohort const preTeams = teams.filter(t => t.preDone); const postTeams = teams.filter(t => t.postDone); const avgPre = preTeams.length ? avg(preTeams.map(t=>t.preScore)) : null; const avgPost = postTeams.length ? avg(postTeams.map(t=>t.postScore)) : null; return (
{/* Top KPI strip — 5 stats: Pre + Post + Reliability + cohort scale */}
{/* Trend + Pre/Post comparison + risk distribution */}
{cohortDimensions.map((d) => (
{d.short} {d.avg != null ? d.avg.toFixed(1) : '—'}
{d.label}
{d.questionRange} · {d.items} items
min {d.min != null ? d.min.toFixed(1) : '—'} max {d.max != null ? d.max.toFixed(1) : '—'} σ {(() => { const vals = teams.map(t => t.dimensions.find(dd=>dd.id===d.id)?.current).filter(v => v != null); return vals.length ? stddev(vals).toFixed(2) : '—'; })()}
))}
{/* Team × Construct matrix */}

Team × Construct matrix

{filtered.length} of {teams.length} projects · click any project to drill into its breakdown

{sorted.map(t => onSelectTeam(t.id)}/>)}
{/* Open-ended responses */} (t.themes||[]).map(text => ({text, q: "What would you keep or change?", sentiment: 0, theme: t.short})))}/> {/* Activity log */}
); } function MeetingProjectCard({ team, onClick }) { const meetings = team.meetings || []; if (!meetings.length) return null; const last = meetings[meetings.length - 1]; const first = meetings[0]; const trend = (last?.score != null && first?.score != null) ? last.score - first.score : 0; const totalResp = meetings.reduce((s, m) => s + (m.responses || 0), 0); const totalAtt = meetings.reduce((s, m) => s + (m.attendees || 0), 0); const respRate = totalAtt ? totalResp / totalAtt : 0; const c = healthColor(team.meetingAvg); return ( ); } function MeetingTrendChart({ data, width = 580, height = 210 }) { const padL = 38, padR = 18, padT = 20, padB = 30; const w = width - padL - padR, h = height - padT - padB; const min = 1, max = 5; const xs = i => padL + (i / (data.length - 1 || 1)) * w; const ys = v => padT + h - ((v - min) / (max - min)) * h; const validData = data.filter(d => d.score != null); const last = validData.length ? validData[validData.length - 1] : null; const path = validData.map((d, i) => { const origI = data.indexOf(d); return `${i ? "L" : "M"}${xs(origI)},${ys(d.score)}`; }).join(" "); if (!validData.length) return
No meeting data yet
; return ( {[1,2,3,4,5].map(v => ( {v} ))} {last && } {validData.map((d, i) => { const origI = data.indexOf(d); return ( {d.label} ); })} ); } // Round helper (data.js' r1 not exported) function r1(x) { return Math.round(x * 10) / 10; } // ============================================================ // PROJECT DETAIL — Health // ============================================================ function HealthProjectDetail({ team, data, onBack }) { const { dimensions } = data; const tAvg = avg(team.dimensions.map(d => d.current)); const sampleResponses = OPEN_RESPONSES_HEALTH.slice(0, 6); return (
{team.code} · {team.region} · {team.sector}

{team.name}

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

{/* Pre / Post baseline pair */}
{/* Trend + radar */}
({...w, label: `W${w.week}`}))} compare={data.cohortWeekly.map(w => ({...w, label: `W${w.week}`}))} compareLabel="Cohort avg" width={680} height={220}/>
{/* Per-dimension breakdown */}
{team.dimensions.map(d => (
{d.short}
{d.label}
{d.current != null ? d.current.toFixed(1) : "—"}

{d.blurb}

{d.questions.map((q, qi) => (
))}
{d.questionRange}{d.items} items
))}
{/* Open-ended for this team */} ({text, q: "What would you keep or change?", sentiment: 0}))}/>
); } // ============================================================ // PROJECT DETAIL — Meeting // ============================================================ function MeetingProjectDetail({ team, data, onBack }) { const meetings = team.meetings; const totalResp = meetings.reduce((s, m) => s + m.responses, 0); const totalAtt = meetings.reduce((s, m) => s + m.attendees, 0); const respRate = totalAtt ? totalResp / totalAtt : 0; return (
{team.code} · {team.region} · {team.sector}

{team.name}

{meetings.length} meetings logged · {totalResp} of {totalAtt} attendees responded ({Math.round(respRate*100)}%)

m.score)).toFixed(1) : "—"} sub={(() => { const m = meetings.reduce((a,b)=>b.score>a.score?b:a); return m.date; })()}/> m.score)).toFixed(1) : "—"} sub={(() => { const m = meetings.reduce((a,b)=>b.score
Date
Attendees
Responses
Rate
Score
{meetings.map(m => { const rate = m.attendees ? m.responses / m.attendees : 0; return (
{m.date}
{m.attendees}
{m.responses}
{Math.round(rate*100)}%
{m.score != null ? m.score.toFixed(1) : "—"}
); })}
{team.meetingItems.map((it, i) => (
{it.q}
{it.score != null ? it.score.toFixed(1) : '—'}
))}
({text, q: "What would you keep or change?", sentiment: 0}))}/>
); } // ============================================================ // SHARED COMPONENTS // ============================================================ function ScoreTile({ label, value, sub, accent, highlight }) { return (
{label}
{value}
{sub}
); } function BigStat({ label, value, sub, accent }) { return (
{label}
{value}
{sub}
); } function KV({ label, v }) { return
{label}{v}
; } function RiskRing({ summary, teams, onSelectTeam }) { const total = teams.length; const arc = (start, frac, color) => { if (frac <= 0) return null; const r = 56, cx = 70, cy = 70; const a0 = -Math.PI/2 + start*2*Math.PI; const a1 = a0 + frac*2*Math.PI; const large = frac > 0.5 ? 1 : 0; const x0 = cx + r*Math.cos(a0), y0 = cy + r*Math.sin(a0); const x1 = cx + r*Math.cos(a1), y1 = cy + r*Math.sin(a1); return ; }; const c = summary.riskCounts; const fH = c.healthy/total, fW = c.watch/total, fC = c.critical/total; return (
{arc(0, fH, riskColor("healthy"))} {arc(fH, fW, riskColor("watch"))} {arc(fH+fW, fC, riskColor("critical"))} {total} TEAMS
{teams.filter(t=>t.risk!=="healthy").slice(0,3).map(t => ( ))}
); } function RiskRow({label, count, total, color}) { const pct = (count/total)*100; return (
{label}
{count}
); } // Open-ended panel — sentiment-coded responses with theme grouping function OpenEndedPanel({ responses, byTheme }) { const [filter, setFilter] = useStateRC("all"); const filtered = filter === "all" ? responses : filter === "pos" ? responses.filter(r => r.sentiment > 0) : filter === "neg" ? responses.filter(r => r.sentiment < 0) : responses.filter(r => r.sentiment === 0); const counts = { all: responses.length, pos: responses.filter(r => r.sentiment > 0).length, neg: responses.filter(r => r.sentiment < 0).length, neu: responses.filter(r => r.sentiment === 0).length, }; return (
Anonymized · open-text · coded by theme & sentiment
{filtered.map((r, i) => (
0 ? "oklch(0.62 0.10 145)" : r.sentiment < 0 ? "oklch(0.58 0.15 28)" : SUBINK}}/>
{byTheme && r.theme && {r.theme}} {r.q}

"{r.text}"

0 ? "oklch(0.42 0.10 145)" : r.sentiment < 0 ? "oklch(0.5 0.13 28)" : SUBINK}}> {r.sentiment > 0 ? "Positive" : r.sentiment < 0 ? "Concern" : "Neutral"}
))}
); } // Response activity log — used in both health and meeting tabs function ResponseLog({ teams, kind }) { const all = []; const today = new Date(); teams.forEach((t, ti) => { const lastWk = t.weekly[t.weekly.length - 1]; const n = Math.min(lastWk.responses, 3); for (let i = 0; i < n; i++) { const minsAgo = (ti * 13 + i * 19) % 360 + 4; const dt = new Date(today.getTime() - minsAgo * 60000); const dur = `${4 + ((t.size + i) % 4)}m ${((t.size * 7 + i * 13) % 60).toString().padStart(2,'0')}s`; const isMeeting = kind === "meeting" ? true : (kind === "health" ? false : i % 3 === 0); if (kind === "meeting" && !isMeeting) continue; if (kind === "health" && isMeeting && i !== 0) continue; all.push({ id: `${t.id}-${i}-${kind}`, team: t, instrument: isMeeting ? "Meeting Effectiveness" : "Team Health", wave: `Wk ${lastWk.week}`, submitted: dt, duration: dur, score: lastWk.score, complete: i % 6 !== 5, minsAgo, }); } }); all.sort((a, b) => b.submitted - a.submitted); const rows = all.slice(0, 15); const fmtAgo = (m) => m < 60 ? `${m}m ago` : `${Math.floor(m/60)}h ${m%60}m ago`; const fmtTime = (d) => d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }); return (
Project
Instrument
Wave
Submitted
Dur
Score
Status
{rows.map(r => (
{r.team.short}
{r.instrument}
{r.wave}
{fmtTime(r.submitted)} · {fmtAgo(r.minsAgo)}
{r.duration}
{r.score != null ? r.score.toFixed(1) : "—"}
{r.complete ? Complete : Partial}
))}
); } Object.assign(window, { ResearcherView });