Files
Sdk_TV_app/templates/league_scoreboard_display.html

519 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title data-i18n="league.league_results">League Results</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/navbar.css">
<link rel="stylesheet" href="/static/css/buttons.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/responsive.css">
<link rel="stylesheet" href="/static/css/pages/league_scoreboard_display.css">
<script src="/static/js/i18n.js"></script>
</head>
<body>
<div class="navbar">
<div class="navbar-brand">
<div class="navbar-title">🎖️ <span data-i18n="league.league_results">League Results</span></div>
</div>
<div class="navbar-controls">
<button class="nav-btn" onclick="exportLeagueJSON()">💾 <span data-i18n="general.export">Export JSON</span></button>
<button class="nav-btn" onclick="window.print()">🖨️ <span data-i18n="general.print">Print</span></button>
<button class="nav-btn" id="btnTvView" onclick="toggleTvView()" title="Preklopi pogled na TV zaslonu">📺</button>
<a href="/" class="nav-btn">✕</a>
</div>
</div>
<div class="league-container">
<!-- Left Column -->
<div class="left-column">
<!-- League Header -->
<div class="league-header tournament-{{ league.tournament_type if league and league.tournament_type else '20_targets' }}">
<img src="/static/logo.png" alt="Logo" class="header-logo" onerror="this.style.display='none'" />
<div class="league-title">
{% if league and league.tournament_type == '4_targets' %}
🎯 <span data-i18n="league.league_championship">League Championship</span>
{% elif league and league.tournament_type == '40_targets' %}
💪 <span data-i18n="league.league_championship">League Championship</span>
{% else %}
⚡ <span data-i18n="league.league_championship">League Championship</span>
{% endif %}
</div>
<div class="league-subtitle" data-i18n="league.final_rankings_best_4_of_5">Final Rankings - Best 4 of 5 Tournaments</div>
<div class="league-meta">
<div class="meta-item">
<span class="meta-number" id="participantCount">0</span>
<span class="meta-label" data-i18n="players.players">Igralci</span>
</div>
<div class="meta-item">
<span class="meta-number" id="tournamentCount">5</span>
<span class="meta-label" data-i18n="tournament.tournaments">Turnirji</span>
</div>
<div class="meta-item">
<span class="meta-number" id="targetCount">0</span>
<span class="meta-label" data-i18n="tournament.targets">Targets</span>
</div>
<div class="meta-item">
<span class="meta-number" id="topScore">0</span>
<span class="meta-label" data-i18n="results.highest_score">Highest Score</span>
</div>
<div class="meta-item">
<span class="meta-number" id="mostTens">0</span>
<span class="meta-label" data-i18n="results.most_tens">Most 10s</span>
</div>
</div>
</div>
<!-- Champion Section -->
<div class="champion-section" id="championSection">
<div class="champion-title" data-i18n="league.league_champions">🎖️ League Champions</div>
<div class="champion-container" id="championContainer">
<!-- Champion cards will be generated by JavaScript -->
</div>
</div>
</div>
<!-- Right Column -->
<div class="right-column">
<div class="table-header">
<h3 class="table-title" data-i18n="league.5_tournament_league">📊 5 Turnirska Liga - Štejejo Najboljši 4</h3>
<!-- Print-only title -->
<div class="print-title" style="display: none;">
🎖️ <span data-i18n="league.league_results">League Results</span>
</div>
</div>
<div class="table-container">
<table class="league-table">
<thead>
<tr>
<th rowspan="2" class="rank-cell" data-i18n="results.position">Rank</th>
<th rowspan="2" class="player-col" data-i18n="league.participant">Udeleženec</th>
<th colspan="5" class="tournament-header-group" data-i18n="league.tournament_scores">Tournament Scores</th>
<th rowspan="2" class="final-col"><span data-i18n="league.final">Final</span><br><small>(<span data-i18n="league.best_4">Best 4</span>)</small></th>
<th rowspan="2" class="tens-col"><span data-i18n="league.total_10s">10k</span></th>
</tr>
<tr class="tournament-header-group">
<th class="tournament-col">T1</th>
<th class="tournament-col">T2</th>
<th class="tournament-col">T3</th>
<th class="tournament-col">T4</th>
<th class="tournament-col">T5</th>
</tr>
</thead>
<tbody id="leagueTableBody">
<!-- Table rows will be generated by JavaScript -->
</tbody>
</table>
</div>
<div class="calculation-legend">
<div class="legend-title" data-i18n="league.scoring_legend">📖 Legenda Točkovanja:</div>
<div class="legend-items">
<div class="legend-item">
<span class="legend-box legend-counted"></span>
<span data-i18n="league.counted_score">Counted Score</span>
</div>
<div class="legend-item">
<span class="legend-box legend-excluded"></span>
<span data-i18n="league.excluded_worst">Excluded (Worst)</span>
</div>
<div class="legend-item">
<span class="legend-box legend-joker"></span>
<span data-i18n="league.joker_used">{{ translations.league.joker_used if translations else 'Joker Used' }}</span>
</div>
<div class="legend-item">
<span class="legend-box legend-final"></span>
<span data-i18n="league.final_score">Final Score</span>
</div>
<div class="legend-item">
<span class="legend-box legend-tens"></span>
<span data-i18n="league.total_10s_tiebreaker">Total 10s (Tiebreaker)</span>
</div>
</div>
</div>
<div class="stats-footer">
<div class="stat-item">
<div class="stat-value" id="footerParticipants">0</div>
<div data-i18n="players.players">Igralci</div>
</div>
<div class="stat-item">
<div class="stat-value" id="footerTournaments">5</div>
<div data-i18n="tournament.tournaments">Turnirji</div>
</div>
<div class="stat-item">
<div class="stat-value" id="footerHighest">0</div>
<div data-i18n="results.highest_score">Highest Score</div>
</div>
<div class="stat-item">
<div class="stat-value" id="footerAverage">0</div>
<div data-i18n="league.average_final">Povprečni Končni</div>
</div>
<div class="stat-item">
<div class="stat-value" id="footerMostTens">0</div>
<div data-i18n="results.most_tens">Most Tens</div>
</div>
<div class="stat-item">
<div class="stat-value" id="footerDate">Today</div>
<div data-i18n="general.date">Datum</div>
</div>
</div>
</div>
</div>
<script>
// Get data from template
const participants = {{ participants|tojson }};
const league = {{ league|tojson }};
console.log('League participants:', participants);
console.log('League data:', league);
// Calculate 10s from targets in tournament results
function calculateTensFromTargets(targets) {
let tensCount = 0;
if (!targets || typeof targets !== 'object') {
return 0;
}
for (let targetNum in targets) {
const target = targets[targetNum];
if (target && typeof target === 'object') {
for (let shotKey in target) {
if (shotKey.startsWith('shot') && target[shotKey] === 10) {
tensCount++;
}
}
}
}
return tensCount;
}
// Calculate total 10s across all tournaments for a participant
function calculateTotalTensForParticipant(participant) {
let totalTens = 0;
// Method 1: Check if backend already calculated total_tens
if (participant.total_tens !== undefined && participant.total_tens !== null) {
return participant.total_tens;
}
// Method 2: Try to sum up tens_count from tournament_results
if (participant.tournament_results && Array.isArray(participant.tournament_results)) {
participant.tournament_results.forEach(result => {
if (result.participated && result.tens_count !== undefined) {
totalTens += result.tens_count;
}
});
if (totalTens > 0) {
return totalTens;
}
}
// Method 3: Try to calculate from targets data (usually not available)
if (participant.tournament_results && Array.isArray(participant.tournament_results)) {
participant.tournament_results.forEach(result => {
if (result.targets && result.participated) {
totalTens += calculateTensFromTargets(result.targets);
}
});
}
return totalTens;
}
// Process participant data and add calculated fields
function processParticipants() {
if (!participants || !Array.isArray(participants)) {
console.error('Invalid participants data:', participants);
return [];
}
return participants.map(participant => {
const totalTens = calculateTotalTensForParticipant(participant);
return {
...participant,
total_tens: totalTens
};
});
}
// Calculate best 4 scoring logic for 5 tournaments
function calculateBest4Logic(participant) {
if (!participant.tournament_results || !Array.isArray(participant.tournament_results)) {
return { allScores: [], best4: [], excluded: [], excludedIndices: [] };
}
const participatedResults = participant.tournament_results.filter(r => r.participated);
const allScores = participatedResults.map(r => r.score);
if (allScores.length <= 4) {
return { allScores, best4: allScores, excluded: [], excludedIndices: [] };
}
// Create array of score objects with original indices
const scoresWithIndices = allScores.map((score, index) => ({ score, originalIndex: index }));
// Sort by score (highest first)
const sortedScoresWithIndices = [...scoresWithIndices].sort((a, b) => b.score - a.score);
// Take best 4 and excluded (worst)
const best4WithIndices = sortedScoresWithIndices.slice(0, 4);
const excludedWithIndices = sortedScoresWithIndices.slice(4);
const best4 = best4WithIndices.map(item => item.score);
const excluded = excludedWithIndices.map(item => item.score);
const excludedIndices = excludedWithIndices.map(item => item.originalIndex);
return { allScores, best4, excluded, excludedIndices };
}
// Generate champion HTML
function generateChampionHTML(processedParticipants) {
const topThree = processedParticipants.slice(0, 3);
return topThree.map(participant => {
const medal = participant.rank === 1 ? '🥇' :
participant.rank === 2 ? '🥈' :
participant.rank === 3 ? '🥉' : '';
return `
<div class="champion-card rank-${participant.rank}">
<div class="rank-display">
<div class="medal">${medal}</div>
</div>
<div class="participant-info">
<div class="participant-name">${participant.name}</div>
</div>
<div class="score-display">
<div class="score-number">${participant.final_score}</div>
<div class="tens-count">🎯 ${participant.total_tens}</div>
</div>
</div>
`;
}).join('');
}
// Generate table HTML for 5 tournaments only
function generateTableHTML(processedParticipants) {
return processedParticipants.map(participant => {
const rankClass = participant.rank <= 3 ? `rank-${participant.rank}` : 'rank-other';
const medal = participant.rank === 1 ? '🥇' :
participant.rank === 2 ? '🥈' :
participant.rank === 3 ? '🥉' : '';
const best4Logic = calculateBest4Logic(participant);
// Generate ONLY 5 tournament score cells (T1-T5)
let tournamentCells = '';
let participatedTournamentIndex = 0; // Track index in participated tournaments only
for (let tournamentNum = 1; tournamentNum <= 5; tournamentNum++) {
const result = participant.tournament_results?.find(r => r.tournament === tournamentNum);
if (!result) {
tournamentCells += '<td>-</td>';
} else if (!result.participated || result.joker) {
tournamentCells += '<td><span class="tournament-score joker">🃏</span></td>';
} else {
const score = result.score;
const tensCount = result.tens_count || 0;
// Check if this specific tournament index should be excluded
const isExcluded = best4Logic.excludedIndices.includes(participatedTournamentIndex) && best4Logic.allScores.length > 4;
const scoreClass = isExcluded ? 'excluded' : 'counted';
tournamentCells += `<td><span class="tournament-score ${scoreClass}">${score}</span></td>`;
participatedTournamentIndex++; // Only increment for participated tournaments
}
}
return `
<tr>
<td class="rank-cell ${rankClass}">
${participant.rank} ${medal}
</td>
<td class="player-cell">
<div class="player-name">${participant.name}</div>
</td>
${tournamentCells}
<td class="final-score-cell">${participant.final_score}</td>
<td class="tens-count-cell">🎯 ${participant.total_tens}</td>
</tr>
`;
}).join('');
}
// Update statistics
function updateStatistics(processedParticipants) {
const count = processedParticipants.length;
const totalTournaments = 5; // Fixed to 5 tournaments
const tournamentType = league ? league.tournament_type : '20_targets';
const targetCount = tournamentType === '40_targets' ? 40 :
tournamentType === '4_targets' ? 4 : 20;
const topScore = count > 0 ? processedParticipants[0].final_score : 0;
const mostTens = count > 0 ? Math.max(...processedParticipants.map(p => p.total_tens)) : 0;
const averageFinal = count > 0 ? Math.round(processedParticipants.reduce((sum, p) => sum + p.final_score, 0) / count) : 0;
// Format date from YYYY-MM-DD to DD-MM-YY
function formatFooterDate(dateString) {
if (!dateString) return '';
const parts = dateString.split('-');
if (parts.length !== 3) return dateString;
const year = parts[0].slice(-2);
const month = parts[1];
const day = parts[2];
return `${day}-${month}-${year}`;
}
const currentDate = league && league.finished_at ?
formatFooterDate(league.finished_at.substring(0, 10)) :
formatFooterDate(new Date().toISOString().substring(0, 10));
// Update header stats
document.getElementById('participantCount').textContent = count;
document.getElementById('targetCount').textContent = targetCount;
document.getElementById('topScore').textContent = topScore;
document.getElementById('mostTens').textContent = mostTens;
// Update footer stats
document.getElementById('footerParticipants').textContent = count;
document.getElementById('footerHighest').textContent = topScore;
document.getElementById('footerAverage').textContent = averageFinal;
document.getElementById('footerMostTens').textContent = mostTens;
document.getElementById('footerDate').textContent = currentDate;
}
// Initialize the page
function initializePage() {
try {
const processedParticipants = processParticipants();
if (processedParticipants.length === 0) {
console.error('No participants to display');
document.getElementById('championSection').style.display = 'none';
return;
}
// Generate champion cards (top 3 only)
if (processedParticipants.length >= 3) {
document.getElementById('championContainer').innerHTML = generateChampionHTML(processedParticipants);
} else {
document.getElementById('championSection').style.display = 'none';
}
// Generate table
document.getElementById('leagueTableBody').innerHTML = generateTableHTML(processedParticipants);
// Update statistics
updateStatistics(processedParticipants);
console.log('League results initialized successfully');
} catch (error) {
console.error('Error initializing league results:', error);
}
}
// Initialize when page loads
document.addEventListener('DOMContentLoaded', initializePage);
// Export league data as JSON
function exportLeagueJSON() {
const leagueData = {{ league | tojson | safe }};
// Wrap in archive format for compatibility
const archiveData = {
league: leagueData,
archived_at: new Date().toISOString()
};
const dataStr = JSON.stringify(archiveData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
// Generate filename with league info
const leagueId = leagueData.league_id || 'league';
const date = new Date().toISOString().slice(0, 10);
link.download = `${leagueId}_${date}.json`;
link.click();
URL.revokeObjectURL(url);
}
// Keyboard shortcuts
document.addEventListener('keydown', function(event) {
if (event.key === 'r' || event.key === 'R') {
event.preventDefault();
window.location.reload();
} else if (event.key === 'p' || event.key === 'P') {
event.preventDefault();
window.print();
}
});
// ── TV VIEW TOGGLE ────────────────────────────────────────────────────────
const isTvDisplay = new URLSearchParams(window.location.search).get('tv') === '1';
let currentTvView = 'results';
function updateTvViewBtn(view) {
currentTvView = view;
const btn = document.getElementById('btnTvView');
if (!btn) return;
btn.style.background = (view === 'results') ? '#28a745' : '';
btn.style.color = (view === 'results') ? 'white' : '';
btn.style.borderColor = (view === 'results') ? '#1e7e34' : '';
}
async function toggleTvView() {
const newView = currentTvView === 'results' ? 'cameras' : 'results';
try {
const payload = { view: newView };
if (newView === 'results') {
// Pass the current page path so the TV follows to this league archive, not a tournament archive
payload.tv_url = window.location.pathname;
}
await fetch('/api/tv/view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
updateTvViewBtn(newView);
if (isTvDisplay && newView === 'cameras') {
window.location.href = '/';
}
} catch (e) { console.error(e); }
}
// Poll to stay in sync and follow back-redirect when in TV mode
setInterval(async () => {
if (document.visibilityState !== 'visible') return;
try {
const r = await fetch('/api/dashboard/state');
if (!r.ok) return;
const d = await r.json();
updateTvViewBtn(d.tv_view || 'cameras');
if (isTvDisplay && d.tv_view !== 'results') {
if (d.tv_view === 'draft' && d.tournament_active) {
window.location.href = '/tournament/draft?tv=1';
} else {
window.location.href = '/';
}
}
} catch (e) {}
}, 3000);
fetch('/api/dashboard/state').then(r => r.json()).then(d => updateTvViewBtn(d.tv_view || 'cameras')).catch(() => {});
</script>
</body>
</html>