/* organizer-monitor-techday.jsx — Tech-Day variant of OrgMonitorPage * Renders agenda, speakers/decks, leaderboard, engagement metrics * Branched from OrgMonitorPage when event_type === 'tech-day' */ // Inline phase badge (PhaseBadge in organizer-pages.jsx is not on window; modules are isolated) function TDPhaseBadge(props) { var phase = props.phase || 'registration'; var label = phase.charAt(0).toUpperCase() + phase.slice(1); var color = phase === 'playing' ? 'var(--ok)' : (phase === 'draft' ? 'var(--wn)' : 'var(--brand)'); var bg = phase === 'playing' ? 'var(--ok-soft)' : (phase === 'draft' ? 'var(--wn-soft)' : 'var(--brand-soft)'); return React.createElement('span', { style: { fontFamily: 'var(--f-m)', fontSize: '10px', letterSpacing: '.1em', textTransform: 'uppercase', fontWeight: 600, padding: '3px 9px', borderRadius: 'var(--r-p)', background: bg, color: color, border: '1px solid ' + color, } }, label); } window.OrgMonitorTechDay = function OrgMonitorTechDay(props) { var gameId = props.gameId; var detail = props.detail || {}; var onSelectGame = props.onSelectGame || function() {}; var showToast = props.showToast || function() {}; var onRefresh = props.onRefresh || function() {}; var game = detail.game || {}; var leaderboard = detail.leaderboard || []; var players = detail.players || []; var phase = detail.phase || game.current_phase || 'registration'; var eventId = game.event_id || ''; var displayCode = game.join_code || gameId; var _ev = React.useState(null); var eventData = _ev[0]; var setEventData = _ev[1]; var _sp = React.useState([]); var speakers = _sp[0]; var setSpeakers = _sp[1]; var _ag = React.useState([]); var agenda = _ag[0]; var setAgenda = _ag[1]; var _qs = React.useState([]); var questions = _qs[0]; var setQuestions = _qs[1]; var _ld = React.useState(true); var loading = _ld[0]; var setLoading = _ld[1]; var _td = React.useState(null); var tdState = _td[0]; var setTdState = _td[1]; var _nw = React.useState(Date.now()/1000); var nowTs = _nw[0]; var setNowTs = _nw[1]; var _bz = React.useState(false); var busy = _bz[0]; var setBusy = _bz[1]; var _tp1 = React.useState(10); var testPlayerCount = _tp1[0]; var setTestPlayerCount = _tp1[1]; var _tp2 = React.useState(false); var generatingPlayers = _tp2[0]; var setGeneratingPlayers = _tp2[1]; // Per-agenda counts for "Open questions" — {agenda_id: {n_tech, n_theme}} var _qc = React.useState({}); var qCounts = _qc[0]; var setQCounts = _qc[1]; function getQCount(agendaId, key, dflt) { var rec = qCounts[agendaId] || {}; var v = rec[key]; return (v == null || v === '') ? dflt : v; } function setQCount(agendaId, key, val) { setQCounts(function(prev) { var next = Object.assign({}, prev); var rec = Object.assign({}, next[agendaId] || {}); rec[key] = val; next[agendaId] = rec; return next; }); } /* ── Test player helpers (admin/dev tool) ── */ function handleGenerateTestPlayers() { setGeneratingPlayers(true); AdminApi.generateTestPlayers(gameId, testPlayerCount) .then(function(res) { var cnt = (res && res.count) || testPlayerCount; showToast(cnt + ' test players created', 'ok'); setGeneratingPlayers(false); onRefresh(); }) .catch(function(err) { showToast('Failed: ' + ((err && err.message) || err), 'err'); setGeneratingPlayers(false); }); } function handleResetPlayers() { if (!window.confirm('Delete ALL players from this game? This cannot be undone.')) return; AdminApi.resetPlayers(gameId) .then(function() { showToast('All players deleted', 'ok'); onRefresh(); }) .catch(function(err) { showToast('Failed: ' + ((err && err.message) || err), 'err'); }); } function handleResetGame() { if (!window.confirm('FULL RESET of game ' + gameId + '?\n\nThis deletes:\n • All players & scores\n • All tech-day quiz answers\n • Active talk / questions-open state\n • Draft state, modules, custom codes\n\nGame phase resets to "registration". Cannot be undone.')) return; AdminApi.resetGame(gameId) .then(function() { showToast('Game fully reset', 'ok'); onRefresh(); }) .catch(function(err) { showToast('Reset failed: ' + ((err && err.message) || err), 'err'); }); } /* ── Fetch tech-day specific data ── */ React.useEffect(function() { if (!eventId) { setLoading(false); return; } function refresh() { Promise.all([ AdminApi.getEvent(eventId).catch(function() { return null; }), AdminApi.listEventSpeakers(eventId).catch(function() { return []; }), AdminApi.listEventAgenda(eventId).catch(function() { return []; }), AdminApi.listEventQuestions(eventId).catch(function() { return []; }), ]).then(function(r) { if (r[0]) setEventData(r[0]); setSpeakers(Array.isArray(r[1]) ? r[1] : []); setAgenda(Array.isArray(r[2]) ? r[2] : []); setQuestions(Array.isArray(r[3]) ? r[3] : []); setLoading(false); }); } refresh(); var t = setInterval(refresh, 15000); return function() { clearInterval(t); }; }, [eventId]); /* ── Poll tech-day flow state every 3s ── */ React.useEffect(function() { if (!gameId) return; function pollState() { AdminApi.techDayGetState(gameId).then(setTdState).catch(function() {}); } pollState(); var t = setInterval(pollState, 3000); var clk = setInterval(function() { setNowTs(Date.now() / 1000); }, 1000); return function() { clearInterval(t); clearInterval(clk); }; }, [gameId]); /* ── Action handlers ── */ function withBusy(promise, successMsg) { setBusy(true); return promise .then(function(res) { if (res && res.game_phase !== undefined) setTdState(res); if (successMsg) showToast(successMsg, 'ok'); return res; }) .catch(function(err) { var msg = (err && err.message) || 'Action failed'; showToast(msg, 'err'); throw err; }) .finally(function() { setBusy(false); }); } function handleSetActive(agendaId) { return withBusy(AdminApi.techDaySetActiveTalk(gameId, agendaId), 'Active talk updated'); } function handleOpenQuestions(agendaId, opts) { return withBusy(AdminApi.techDayOpenQuestions(gameId, agendaId, opts || {}), 'Questions opened'); } function handleCloseQuestions() { return withBusy(AdminApi.techDayCloseQuestions(gameId), 'Questions closed'); } function handleStartDraft() { if (!confirm('Start draft? Requires at least 2 captains pre-assigned.')) return; setBusy(true); AdminApi.startDraft(gameId) .then(function() { showToast('Draft started', 'ok'); }) .catch(function(err) { showToast((err && err.message) || 'Could not start draft', 'err'); }) .finally(function() { setBusy(false); }); } function handleResetDraft() { if (!confirm('Restart draft? This clears all team assignments (keeps captains) and re-runs the snake draft from round 1.')) return; setBusy(true); AdminApi.resetDraft(gameId) .then(function() { showToast('Draft reset — ready to restart', 'ok'); onRefresh(); }) .catch(function(err) { showToast((err && err.message) || 'Could not reset draft', 'err'); }) .finally(function() { setBusy(false); }); } function handleBeginEvent() { if (!confirm('Begin event? This flips the game to playing phase and auto-assigns any remaining players.')) return; setBusy(true); AdminApi.setGamePhase(gameId, 'playing') .then(function() { showToast('Event started', 'ok'); }) .catch(function(err) { showToast((err && err.message) || 'Could not begin event', 'err'); }) .finally(function() { setBusy(false); }); } /* ── Derived metrics ── */ var speakerById = {}; speakers.forEach(function(s) { speakerById[s.speaker_id] = s; }); // Normalize agenda items: speaker_ids is the array; older entries may have speaker_id function agendaSpeakerIds(item) { if (Array.isArray(item.speaker_ids) && item.speaker_ids.length) return item.speaker_ids; if (item.speaker_id) return [item.speaker_id]; return []; } var questionsByAgenda = {}; questions.forEach(function(q) { var aid = q.agenda_id || '_unassigned'; if (!questionsByAgenda[aid]) questionsByAgenda[aid] = []; questionsByAgenda[aid].push(q); }); var totalQuestions = questions.length; var aiQuestions = questions.filter(function(q) { return q.source === 'ai'; }).length; var manualQuestions = totalQuestions - aiQuestions; var nonProctorPlayers = players.filter(function(p) { return !p.isProctor; }); /* ── Sorted leaderboard ── */ var sortedLb = leaderboard.slice().sort(function(a, b) { return (b.score || b.total_score || 0) - (a.score || a.total_score || 0); }); /* ── Derived flow data ── */ // NOTE: API returns isCaptain/isProctor (camelCase). p.is_captain would always be undefined. var captains = players.filter(function(p) { return p.isCaptain && !p.isProctor; }); var nonCaptainPlayers = players.filter(function(p) { return !p.isCaptain && !p.isProctor; }); var captainByTeam = {}; captains.forEach(function(p) { if (p.team) captainByTeam[p.team] = p; }); // Resolve theme teams (mirrors organizer-pages.jsx: DB stores 'world-cup' etc., data.js uses 'cup') var THEME_REVERSE_MAP = { 'f1': 'gp', 'world-cup': 'cup', 'nfl': 'nfl', 'realm': 'realm' }; var rawThemeId = (game.theme_id || game.theme || 'cisco'); var gameThemeId = THEME_REVERSE_MAP[rawThemeId] || rawThemeId; var themeObj = (window.THEMES || []).find(function(t) { return t.id === gameThemeId; }) || (window.THEMES || [])[0] || { teams: [] }; var themeTeams = themeObj.teams || []; function handleSetCaptain(email, teamId) { setBusy(true); AdminApi.setCaptain(gameId, email, teamId) .then(function() { showToast('Captain set for ' + teamId, 'ok'); onRefresh(); }) .catch(function(err) { showToast('Failed: ' + ((err && err.message) || err), 'err'); }) .finally(function() { setBusy(false); }); } function handleRemoveCaptain(email, teamId) { setBusy(true); AdminApi.removeCaptain(gameId, email, teamId) .then(function() { showToast('Captain removed', 'ok'); onRefresh(); }) .catch(function(err) { showToast('Failed: ' + ((err && err.message) || err), 'err'); }) .finally(function() { setBusy(false); }); } var teamlessCount = nonProctorPlayers.filter(function(p) { return !p.team; }).length; var draftComplete = phase === 'draft' && teamlessCount === 0; var currentPhase = (tdState && tdState.game_phase) || phase || 'registration'; var activeAgendaId = tdState && tdState.active_agenda_id; var questionsOpen = !!(tdState && tdState.questions_open); var closesAt = tdState && tdState.questions_closes_at; var secondsLeft = (questionsOpen && closesAt) ? Math.max(0, Math.floor(closesAt - nowTs)) : 0; function fmtClock(s) { if (!s || s <= 0) return '0:00'; var m = Math.floor(s / 60); var ss = s % 60; return m + ':' + (ss < 10 ? '0' + ss : ss); } function nextAgendaId(currentId) { var sorted = agenda.slice().sort(function(a, b) { return (a.sort_order || 0) - (b.sort_order || 0); }); var idx = sorted.findIndex(function(a) { return a.agenda_id === currentId; }); if (idx < 0 || idx >= sorted.length - 1) return null; return sorted[idx + 1].agenda_id; } return (
{/* ── Header ── */}
{(eventData && eventData.name) || game.name || gameId}
Tech Day {displayCode} {nonProctorPlayers.length} player{nonProctorPlayers.length !== 1 ? 's' : ''} · {speakers.length} speaker{speakers.length !== 1 ? 's' : ''} · {agenda.length} talk{agenda.length !== 1 ? 's' : ''}
{eventData && (eventData.start_date || eventData.location) ? (
{eventData.start_date || '—'}{eventData.location ? ' · ' + eventData.location : ''}
) : null}
⚙ Manage Tech Day
{loading ? (
Loading tech-day data…
) : (
{/* ── Event Flow controls ── */}

Event Flow

Phase: {currentPhase.toUpperCase()}
{currentPhase === 'registration' ? (
1
Assign captains ({captains.length} / {themeTeams.length || 5})
Pick a captain for each team in the panel below.
2
Start draft
3
Skip draft (random teams)
) : currentPhase === 'draft' ? (
Draft in progress — {nonProctorPlayers.length - teamlessCount} of {nonProctorPlayers.length} players assigned.
{draftComplete ? ( ) : null}
) : (
{activeAgendaId ? ( Active talk: {tdState.active_title || activeAgendaId} {questionsOpen ? ( 🟢 questions open · closes in {fmtClock(secondsLeft)} ) : ( 🔵 questions closed )} ) : ( No active talk. Pick one from the agenda below. )}
{questionsOpen ? ( ) : null}
)}
{/* ── Test Players (registration phase only — dev/admin tool) ── */} {currentPhase === 'registration' && (

Test Players Dev tool

Simulate registered players for testing
For reset operations, see Danger Zone below ↓
)} {/* ── Danger Zone (always visible, all phases) ── */}

⚠ Danger Zone

Destructive operations — confirm before clicking
Keeps game config & modules
Clears players, scores, tech-day answers & active talk · phase → registration
{/* ── Captain Assignment (registration phase only) ── */} {currentPhase === 'registration' && (

Captain Assignment

{captains.length}/{themeTeams.length || 5} assigned · {nonProctorPlayers.length} player{nonProctorPlayers.length !== 1 ? 's' : ''} registered
{themeTeams.length === 0 ? (
No teams found for theme "{gameThemeId}". Check the event theme.
) : nonProctorPlayers.length === 0 ? (
No players registered yet. Share the join code {displayCode} or QR from the projector.
) : (
{themeTeams.map(function(team) { var captain = captainByTeam[team.id]; return (
{team.name}
{captain ? (
★ {captain.name || captain.email}
) : ( )}
); })}
)}
)} {/* ── Agenda timeline ── */}

Agenda

{agenda.length} talk{agenda.length !== 1 ? 's' : ''}
{agenda.length === 0 ? (
No agenda items yet. Add some.
) : (
    {agenda.slice().sort(function(a, b) { return (a.sort_order || 0) - (b.sort_order || 0); }).map(function(item, idx) { var sids = agendaSpeakerIds(item); var resolvedSpeakers = sids.map(function(sid) { return speakerById[sid]; }).filter(Boolean); var qs = questionsByAgenda[item.agenda_id] || []; var hasAnySpeaker = resolvedSpeakers.length > 0 || sids.length > 0; var isActive = activeAgendaId === item.agenda_id; var nextId = nextAgendaId(item.agenda_id); return (
  1. {String(idx + 1).padStart(2, '0')}
    {item.title || 'Untitled talk'}
    {hasAnySpeaker ? ( {resolvedSpeakers.length ? resolvedSpeakers.map(function(s) { return s.name; }).join(', ') : (item.speaker_info || 'Speaker assigned')} ) : ( No speaker )} {item.time ? · {item.time} : null} {item.type ? · {item.type} : null}
    {qs.length} question{qs.length !== 1 ? 's' : ''} {isActive ? ( {questionsOpen ? '🟢 Q&A open · ' + fmtClock(secondsLeft) : '🔵 Active · Q&A closed'} ) : null}
    {currentPhase === 'playing' ? (
    {!isActive ? ( ) : !questionsOpen ? (
    {nextId ? ( ) : null}
    ) : ( {nextId ? ( ) : null} )}
    ) : null}
  2. ); })}
)}
{/* ── Speakers ── */}

Speakers

{speakers.length} total
{speakers.length === 0 ? (
No speakers yet.
) : (
{speakers.map(function(s) { var assignedAgenda = agenda.filter(function(a) { return agendaSpeakerIds(a).indexOf(s.speaker_id) >= 0; }); var qCount = assignedAgenda.reduce(function(sum, a) { return sum + ((questionsByAgenda[a.agenda_id] || []).length); }, 0); return (
{s.name}
{s.email}
{assignedAgenda.length} talk{assignedAgenda.length !== 1 ? 's' : ''} {qCount} Q{qCount !== 1 ? 's' : ''}
); })}
)}
{/* ── Question bank summary ── */}

Question Bank

For post-talk play
{totalQuestions}
Total
{aiQuestions}
AI-drafted
{manualQuestions}
Manual
{/* ── Leaderboard ── */}

Live Leaderboard

{sortedLb.length} ranked
{sortedLb.length === 0 ? (
No scores yet. Players appear here as they answer questions.
) : (
    {sortedLb.slice(0, 10).map(function(row, i) { var name = row.name || row.display_name || row.email || '—'; var score = row.score || row.total_score || 0; return (
  1. {i + 1} {name} {score}
  2. ); })}
)}
)}
); };