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: `
` });
}
export function adminHome(list, who, msg) {
return layout({ title: 'Desk', admin: who, body: `
Organizer desk ${flash(msg)}
Tournaments ${list.length ? `${list.map(x => `${esc(x.name)} ${esc(phaseLabel(x.phase))} ${esc(x.date ?? '')} · ${x.teams} teams `).join('')} ` : 'None yet.
'}
` });
}
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}${label} `;
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.
Set banner
Broadcast
${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 ? `# Match Predicted ${queue.map((q, i) => `${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', '', ` `)} `).join('')}
` : 'Nothing waiting.
'}
` : ''}
Recent activity ${events.slice(0, 40).map(e => `${new Date(e.ts).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} ${esc(describe(e, t))} `).join('')}
`;
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]}
− +
− +
Mark final
Save sets
${esc(t.teamName(m.teamA))} ${esc(t.teamName(m.teamB))} Forfeits
${t.courtCount > 1 ? `${Array.from({ length: t.courtCount }, (_, i) => i + 1).filter(n => n !== m.court).map(n => `${n} `).join('')} Move to court ` : ''}
`;
}
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.
${esc(url)}
Print Back to desk
` });
}