import { esc } from '../http.js'; import { layout, flash } from './layout.js'; import { phaseLabel } from './public.js'; export function loginPage(error = null, next = '/admin') { return layout({ title: 'Organizer sign in', body: `

Organizer desk

${flash(error, 'error')}
` }); } export function adminHome(list, who, msg) { return layout({ title: 'Desk', admin: who, body: `

Organizer desk

${flash(msg)}

Tournaments

${list.length ? `` : '

None yet.

'}

New tournament

` }); } export function adminTournament({ t, engine }, who, origin, msg, err, events) { const s = t.phase; const teams = [...t.teams.values()].sort((a, b) => a.seed - b.seed); const live = engine.courts.map(c => ({ c, m: c.matchId ? t.match(c.matchId) : null })); const queue = s === 'live' ? engine.queue().slice(0, 8) : []; const generated = t.matches.length > 0; const poolsDone = t.pools.length && t.matches.filter(m => m.stage === 'pool').every(m => ['final', 'forfeit', 'void'].includes(m.status)); const hasBracket = t.matches.some(m => m.stage !== 'pool'); const act = (path, label, cls = '', extra = '') => `
${extra}
`; const body = `
Desk · ${esc(t.date ?? '')}

${esc(t.name)} ${esc(phaseLabel(s))}

${t.rules.teamSize}s · to ${t.rules.pointsTo}, win by ${t.rules.winBy}${t.rules.cap ? `, cap ${t.rules.cap}` : ''}, best of ${t.rules.bestOf} · ${t.courtCount} court${t.courtCount > 1 ? 's' : ''} · public page · TV display · QR code

${flash(msg)}${flash(err, 'error')}

Phase

${act('phase', 'Check-in (registration open)', s === 'checkin' ? 'cur' : '', '')} ${act('phase', 'Close registration', s === 'closed' ? 'cur' : '', '')} ${act('phase', 'Go live', s === 'live' ? 'cur' : '', '')} ${act('phase', 'Final', s === 'final' ? 'cur' : '', '')}

The public QR page follows this. Going live starts assigning courts immediately.

${s === 'live' ? `

Courts

${live.map(({ c, m }) => `
Court ${c.number} ${esc(c.status)}${c.pauseReason ? ` · ${esc(c.pauseReason)}` : ''} ${c.status === 'open' ? act('court', 'Pause', '', ``) : act('court', 'Resume', 'primary', ``)}
${m ? scorePad(t, m) : '

No match assigned.

'}
`).join('')}

Queue

${queue.length ? `${queue.map((q, i) => ``).join('')}
#MatchPredicted
${i + 1}${esc(t.teamName(q.match.teamA))} vs ${esc(t.teamName(q.match.teamB))} ${q.match.stage === 'pool' ? `Pool ${esc(q.match.poolId)} R${q.match.round}` : esc(q.match.label ?? `Bracket R${q.match.round}`)}C${q.court} ~${Math.round(q.eta / 60000)}m${act('pushback', 'Push back', '', ``)}
` : '

Nothing waiting.

'}
` : ''}

Play

${!generated ? `
${t.stages.includes('pool') ? `` : ''} ${!t.stages.includes('pool') ? `` : ''}

Uses current seeds (${t.activeTeams().length} active teams). Regenerating is allowed until the first score is entered.

` : ''} ${t.pools.length ? `

Pools: ${t.pools.map(p => `${esc(p.id)} (${p.teams.map(id => esc(t.teamName(id))).join(', ')})`).join(' · ')}

` : ''} ${generated && !t.matches.some(m => ['final', 'forfeit'].includes(m.status)) ? act('regenerate', 'Clear generated play', 'danger') : ''} ${t.pools.length && !hasBracket && t.stages.includes('bracket') ? `
` : ''} ${generated ? `
All matches (${t.matches.length}) — enter or correct any score ${t.matches.filter(m => !m.conditional || m.teamA).map(m => ``).join('')}
IDStageMatchStatusScore
${esc(m.id)}${m.stage === 'pool' ? `Pool ${esc(m.poolId)} R${m.round}` : esc(m.label ?? `${m.stage} R${m.round}`)}${esc(t.teamName(m.teamA))} vs ${esc(t.teamName(m.teamB))}${esc(m.status)}${m.court ? ` C${m.court}` : ''}${m.sets.map(x => x.join('-')).join(', ')} ${m.teamA && m.teamB && !['void', 'bye'].includes(m.status) ? `
` : ''}
` : ''}

Teams (${t.activeTeams().length} active)

${teams.map(x => ``).join('')}
SeedTeamCaptainPlayersCode
${generated ? '' : ''}
${esc(x.phone ?? '')} ${esc(x.code)} ${x.status === 'withdrawn' ? `withdrawn (${esc(x.withdrawalMode)})` : generated ? `
` : act('team', 'Remove', 'danger', ``)}
${!generated ? `
` : '

Play is generated: use Withdraw for teams that leave. To add a team now, clear generated play first.

'}

Recent activity

Danger zone

${act('delete', 'Delete this tournament', 'danger', ` `)}
`; return layout({ title: `${t.name} desk`, admin: who, body, script: '/static/admin.js' }); } function scorePad(t, m) { const live = m.live ?? [0, 0]; return `
${esc(t.teamName(m.teamA))}
${m.stage === 'pool' ? `Pool ${esc(m.poolId)} R${m.round}` : esc(m.label ?? `Bracket R${m.round}`)}
${esc(t.teamName(m.teamB))}
${live[0]}
to ${t.rules.pointsTo}
${live[1]}
${t.courtCount > 1 ? `
` : ''}
`; } function describe(e, t) { const n = id => t.teamName(id); switch (e.type) { case 'team_registered': return `Registered: ${e.name}`; case 'team_updated': return `Edited team: ${e.name}`; case 'team_removed': return `Removed a team`; case 'team_withdrawn': return `Withdrawn: ${e.name} (${e.mode})`; case 'phase': return `Phase → ${e.phase}${e.champion ? ` · champion ${e.champion}` : ''}`; case 'pools_generated': return `Pools generated: ${e.pools.map(p => `${p.id}(${p.teams.length})`).join(' ')}`; case 'bracket_generated': return `${e.bracket} bracket of ${e.size} generated`; case 'match_started': return `Court ${e.court}: ${e.a} vs ${e.b}`; case 'result': { const m = t.match(e.match); return `${m ? `${n(m.teamA)} vs ${n(m.teamB)}` : e.match} → ${n(e.winner)} ${e.score}${e.actor ? ` (${e.actor})` : ''}`; } case 'alert': return `Alert: ${e.text}`; case 'court_paused': return `Court ${e.court} paused${e.reason ? `: ${e.reason}` : ''}`; case 'court_resumed': return `Court ${e.court} resumed`; case 'court_swapped': return `Match moved from court ${e.from} to ${e.to}`; case 'match_pushed_back': return `Match pushed back`; default: return e.type; } } export function qrPage(t, origin, dataUrl, who) { const url = `${origin}/t/${t.slug}`; return layout({ title: `${t.name} QR`, admin: who, body: `

${esc(t.name)}

Scan to register, then to follow the tournament live.

QR code for ${esc(url)}

${esc(url)}

Back to desk

` }); }