// groups.js โ€” standings computed from groups.json + results.json and the 12 // group tables. Only matches with status "finished" count toward standings // (live scores are ignored until full-time). computeStandings() and // isGroupFinished() are reused by bracket.js to resolve the Round of 32. import { getData, flagSrc } from './app.js'; import { t } from './i18n.js'; import { getFavorites } from './storage.js'; // Tiebreak order per complement spec ยง2: points, goal difference, goals for. // Team id alphabetical as a final stable fallback. export function computeStandings() { const { groups, matches, resultByMatchId } = getData(); const tables = {}; for (const [letter, teamIds] of Object.entries(groups)) { tables[letter] = new Map(teamIds.map((id) => [id, { teamId: id, played: 0, won: 0, drawn: 0, lost: 0, gf: 0, ga: 0, gd: 0, points: 0, }])); } for (const match of matches) { if (!match.phase.startsWith('Group ')) continue; const result = resultByMatchId.get(match.id); if (result?.status !== 'finished') continue; const rows = tables[match.phase.slice(6)]; applyResult(rows.get(match.homeTeam), result.homeScore, result.awayScore); applyResult(rows.get(match.awayTeam), result.awayScore, result.homeScore); } const standings = {}; for (const [letter, rows] of Object.entries(tables)) { standings[letter] = [...rows.values()].sort((a, b) => b.points - a.points || b.gd - a.gd || b.gf - a.gf || a.teamId.localeCompare(b.teamId)); } return standings; } function applyResult(row, scored, conceded) { row.played += 1; row.gf += scored; row.ga += conceded; row.gd = row.gf - row.ga; if (scored > conceded) { row.won += 1; row.points += 3; } else if (scored === conceded) { row.drawn += 1; row.points += 1; } else { row.lost += 1; } } export function isGroupFinished(letter) { const { matches, resultByMatchId } = getData(); return matches .filter((m) => m.phase === `Group ${letter}`) .every((m) => resultByMatchId.get(m.id)?.status === 'finished'); } // -------------------------------------------------------------- render export function initGroups() { render(); document.addEventListener('langchange', render); document.addEventListener('favchange', render); } function render() { const standings = computeStandings(); document.getElementById('groups-root').innerHTML = `

${t('standings.legendTop2')} ${t('standings.legendThird')}

${Object.entries(standings).map(([letter, rows]) => groupCardHTML(letter, rows)).join('')}
`; } function groupCardHTML(letter, rows) { const finished = isGroupFinished(letter); const headers = ['played', 'won', 'drawn', 'lost', 'gf', 'ga', 'gd', 'pts'] .map((key) => `${t(`standings.${key}`)}`) .join(''); return `

${t('phase.group')} ${letter}

${finished ? '' : `${t('standings.inProgress')}`}
${headers}${rows.map(standingRowHTML).join('')}
#${t('standings.team')}
`; } function standingRowHTML(row, index) { const team = getData().teamById.get(row.teamId); const fav = getFavorites().includes(team.id); const rankClass = [ index < 2 ? 'row-qualified' : index === 2 ? 'row-third' : '', fav ? 'fav-row' : '', ].filter(Boolean).join(' '); return ` ${index + 1} ${team.name} ${row.played} ${row.won} ${row.drawn} ${row.lost} ${row.gf} ${row.ga} ${row.gd > 0 ? '+' : ''}${row.gd} ${row.points} `; }