414 lines
16 KiB
HTML
414 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title data-i18n="tournament.tournament_results">Tournament 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/results_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="tournament.tournament_results">Tournament Results</span></div>
|
|
</div>
|
|
<div class="navbar-controls">
|
|
<button class="nav-btn" onclick="exportResultsJSON()">💾 <span data-i18n="general.export">Export JSON</span></button>
|
|
<button class="nav-btn" onclick="printResults()">🖨️ <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="tv-container">
|
|
<!-- Left Column -->
|
|
<div class="left-column">
|
|
<!-- Header Section -->
|
|
<div class="results-header tournament-{{ results.tournament_type if results and results.tournament_type else '20_targets' }}">
|
|
<img src="/static/logo.png" alt="Logo" class="header-logo" onerror="this.style.display='none'" />
|
|
<div class="results-title">
|
|
{% if results and results.tournament_type == '4_targets' %}
|
|
🎯 <span data-i18n="tournament.tournament_results">Tournament Results</span>
|
|
{% elif results and results.tournament_type == '40_targets' %}
|
|
💪 <span data-i18n="tournament.tournament_results">Tournament Results</span>
|
|
{% else %}
|
|
⚡ <span data-i18n="tournament.tournament_results">Tournament Results</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="results-subtitle" data-i18n="results.final_results">Final Rankings & Scores</div>
|
|
<div class="results-meta">
|
|
<div class="meta-item">
|
|
<span class="meta-number" id="participantCount">{{ participants|length }}</span>
|
|
<span class="meta-label" data-i18n="tournament.participants">Participants</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-number" id="totalShots">0</span>
|
|
<span class="meta-label" data-i18n="results.total_shots">Total Shots</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-number" id="highestScore">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>
|
|
|
|
<!-- Podium Section -->
|
|
<div class="podium-section" id="podiumSection">
|
|
<div class="podium-title">🏆 <span data-i18n="results.top_3_winners">Top 3 Winners</span></div>
|
|
<div class="podium-container" id="podiumContainer">
|
|
<!-- Podium cards will be generated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column -->
|
|
<div class="right-column">
|
|
<div class="table-header">
|
|
<h3 class="table-title">📊 <span data-i18n="results.rankings">Complete Rankings</span></h3>
|
|
<!-- Print-only title -->
|
|
<div class="print-title" style="display: none;">
|
|
{% if results and results.tournament_type == '4_targets' %}
|
|
🎯 <span data-i18n="tournament.tournament_results">Tournament Results</span>
|
|
{% elif results and results.tournament_type == '40_targets' %}
|
|
💪 <span data-i18n="tournament.tournament_results">Tournament Results</span>
|
|
{% else %}
|
|
⚡ <span data-i18n="tournament.tournament_results">Tournament Results</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="results-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="rank-col" data-i18n="results.position">Rank</th>
|
|
<th class="player-col" data-i18n="players.player">Participant</th>
|
|
<th class="score-col" data-i18n="results.score">Score</th>
|
|
<th class="tens-col" data-i18n="results.tens">10s</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="resultsTableBody">
|
|
<!-- Table rows will be generated by JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="stats-footer">
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="footerParticipants">0</div>
|
|
<div data-i18n="tournament.participants">Participants</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="results.average_score">Average Score</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="footerMostTens">0</div>
|
|
<div data-i18n="results.most_tens">Most 10s</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 results = {{ results|tojson }};
|
|
|
|
console.log('Raw participants data:', participants);
|
|
console.log('Raw results data:', results);
|
|
|
|
// Calculate 10s from shot data
|
|
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 10s from participant data
|
|
function calculateTensForParticipant(participant) {
|
|
// Try different data structures
|
|
if (participant.targets) {
|
|
return calculateTensFromTargets(participant.targets);
|
|
}
|
|
|
|
// If no targets, check if we can get them from results
|
|
if (results && results.participants) {
|
|
const resultParticipant = results.participants[participant.id];
|
|
if (resultParticipant && resultParticipant.targets) {
|
|
return calculateTensFromTargets(resultParticipant.targets);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// 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 tensCount = calculateTensForParticipant(participant);
|
|
console.log(`Player ${participant.name}: ${tensCount} tens`);
|
|
|
|
return {
|
|
...participant,
|
|
tens_count: tensCount
|
|
};
|
|
});
|
|
}
|
|
|
|
// Generate podium HTML
|
|
function generatePodiumHTML(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="podium-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 class="participant-id">ID: ${participant.id}</div>
|
|
</div>
|
|
|
|
<div class="score-display">
|
|
<div class="score-number">${participant.total_score}</div>
|
|
<div class="tens-count">🎯 ${participant.tens_count}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Generate table HTML
|
|
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 ? '🥉' : '';
|
|
|
|
return `
|
|
<tr>
|
|
<td class="rank-cell ${rankClass}">
|
|
${participant.rank} ${medal}
|
|
</td>
|
|
<td class="player-cell">
|
|
<div class="player-name">${participant.name}</div>
|
|
</td>
|
|
<td class="score-cell">${participant.total_score}</td>
|
|
<td class="tens-cell">🎯 ${participant.tens_count}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Update statistics
|
|
function updateStatistics(processedParticipants) {
|
|
const count = processedParticipants.length;
|
|
const totalShots = count * (results && results.tournament_type === '40_targets' ? 80 :
|
|
results && results.tournament_type === '4_targets' ? 20 : 40);
|
|
const highestScore = count > 0 ? processedParticipants[0].total_score : 0;
|
|
const mostTens = count > 0 ? Math.max(...processedParticipants.map(p => p.tens_count)) : 0;
|
|
const averageScore = count > 0 ? Math.round(processedParticipants.reduce((sum, p) => sum + p.total_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 = results && results.created_at ?
|
|
formatFooterDate(results.created_at.substring(0, 10)) :
|
|
formatFooterDate(new Date().toISOString().substring(0, 10));
|
|
|
|
// Update header stats
|
|
document.getElementById('participantCount').textContent = count;
|
|
document.getElementById('totalShots').textContent = totalShots;
|
|
document.getElementById('highestScore').textContent = highestScore;
|
|
document.getElementById('mostTens').textContent = mostTens;
|
|
|
|
// Update footer stats
|
|
document.getElementById('footerParticipants').textContent = count;
|
|
document.getElementById('footerHighest').textContent = highestScore;
|
|
document.getElementById('footerAverage').textContent = averageScore;
|
|
document.getElementById('footerMostTens').textContent = mostTens;
|
|
document.getElementById('footerDate').textContent = currentDate;
|
|
}
|
|
|
|
|
|
// Print function
|
|
function printResults() {
|
|
window.print();
|
|
}
|
|
|
|
// Initialize the page
|
|
function initializePage() {
|
|
try {
|
|
const processedParticipants = processParticipants();
|
|
|
|
if (processedParticipants.length === 0) {
|
|
console.error('No participants to display');
|
|
return;
|
|
}
|
|
|
|
// Generate podium (top 3 only)
|
|
if (processedParticipants.length >= 3) {
|
|
document.getElementById('podiumContainer').innerHTML = generatePodiumHTML(processedParticipants);
|
|
} else {
|
|
document.getElementById('podiumSection').style.display = 'none';
|
|
}
|
|
|
|
// Generate table
|
|
document.getElementById('resultsTableBody').innerHTML = generateTableHTML(processedParticipants);
|
|
|
|
// Update statistics
|
|
updateStatistics(processedParticipants);
|
|
|
|
|
|
console.log('Tournament results initialized successfully');
|
|
console.log('Processed participants:', processedParticipants);
|
|
|
|
} catch (error) {
|
|
console.error('Error initializing tournament results:', error);
|
|
}
|
|
}
|
|
|
|
// Export results as JSON
|
|
function exportResultsJSON() {
|
|
const tournamentData = {{ tournament | tojson | safe }};
|
|
const resultsData = {{ results | tojson | safe }};
|
|
|
|
// Wrap in archive format for compatibility
|
|
const archiveData = {
|
|
tournament: tournamentData,
|
|
results: resultsData,
|
|
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 tournament info
|
|
const tournamentId = tournamentData.tournament_id || 'tournament';
|
|
const date = new Date().toISOString().slice(0, 10);
|
|
link.download = `${tournamentId}_${date}.json`;
|
|
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Initialize when page loads
|
|
document.addEventListener('DOMContentLoaded', initializePage);
|
|
|
|
// 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 {
|
|
await fetch('/api/tv/view', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ view: newView })
|
|
});
|
|
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
|
|
let lastResultsStateHash = null;
|
|
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> |