319 lines
13 KiB
HTML
319 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title data-i18n="players.player_analysis">👤 Player Analysis - Camera Dashboard</title>
|
|
<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/modern_player_analysis.css">
|
|
<script src="/static/js/chart.min.js"></script>
|
|
<script>
|
|
// Fallback: if Chart.js fails to load, create a simple notification
|
|
if (typeof Chart === 'undefined') {
|
|
console.warn('Chart.js failed to load - charts will be unavailable');
|
|
window.Chart = {
|
|
register: () => {},
|
|
Chart: function() {
|
|
return {
|
|
update: () => {},
|
|
destroy: () => {},
|
|
data: {},
|
|
options: {}
|
|
};
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
<script src="/static/js/i18n.js"></script>
|
|
</head>
|
|
<body>
|
|
<!-- Navigation Bar -->
|
|
<div class="navbar">
|
|
<div class="navbar-title">👤 Analiza Igralcev</div>
|
|
<div class="navbar-controls">
|
|
<a href="/archive/player-analysis" class="nav-btn active">👤 <span data-i18n="players.player_analysis">Analiza Igralcev</span></a>
|
|
<a href="/archive" class="nav-btn">📚 <span data-i18n="navigation.archive">Archive</span></a>
|
|
<a href="/" class="nav-btn">✕</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="players-layout">
|
|
|
|
<!-- LEFT: player tree -->
|
|
<div class="players-left-panel">
|
|
<div class="players-panel-header">
|
|
<div class="players-panel-header-title">👥 Igralci</div>
|
|
<div class="players-panel-count" id="playerTreeCount">0</div>
|
|
</div>
|
|
<div class="players-sort-bar">
|
|
<input type="text" class="players-search" id="searchBox" placeholder="🔍 Iskanje…" oninput="filterAndSortPlayers()">
|
|
<div class="players-sort-btns">
|
|
<button class="psort-btn active" data-sort="best_score" onclick="setSort('best_score')">🏆 Skupaj</button>
|
|
<button class="psort-btn" data-sort="total_tens" onclick="setSort('total_tens')">🎯 Desetke</button>
|
|
<button class="psort-btn" data-sort="best_single" onclick="setSort('best_single')">⭐ Enkrat</button>
|
|
</div>
|
|
</div>
|
|
<div class="player-tree-list" id="playerTreeList">
|
|
<div class="loading"><div class="loading-spinner"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT: player detail -->
|
|
<div class="players-right-panel" id="playersRightPanel">
|
|
<div class="player-placeholder" id="playerPlaceholder">
|
|
<div class="player-placeholder-icon">👤</div>
|
|
<div class="player-placeholder-text">Izberi igralca iz seznama</div>
|
|
</div>
|
|
<div id="playerDetailEmbed" style="display:none"></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// Global data
|
|
let playersData = [];
|
|
let tournamentData = [];
|
|
let filteredPlayers = [];
|
|
let currentFilter = 'all';
|
|
let currentSort = 'best_score';
|
|
let playersWithStats = [];
|
|
|
|
// Initialize page
|
|
function initializePage() {
|
|
loadPlayerStats();
|
|
setupEventListeners();
|
|
}
|
|
|
|
// Format date for display
|
|
function formatDate(dateString) {
|
|
if (!dateString) return t('analysis.unknown_date');
|
|
|
|
try {
|
|
const date = new Date(dateString);
|
|
if (isNaN(date.getTime())) {
|
|
// Try to parse filename format YYYYMMDD_HHMMSS
|
|
if (dateString.length >= 8) {
|
|
const year = dateString.substring(0, 4);
|
|
const month = dateString.substring(4, 6);
|
|
const day = dateString.substring(6, 8);
|
|
const parsedDate = new Date(`${year}-${month}-${day}`);
|
|
if (!isNaN(parsedDate.getTime())) {
|
|
return parsedDate.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
}
|
|
return t('analysis.unknown_date');
|
|
}
|
|
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
} catch (error) {
|
|
return 'Unknown Date';
|
|
}
|
|
}
|
|
|
|
// Load player statistics
|
|
async function loadPlayerStats() {
|
|
try {
|
|
const response = await fetch('/api/archive/players/with-stats');
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
playersWithStats = result.players;
|
|
playersData = playersWithStats;
|
|
filteredPlayers = [...playersWithStats];
|
|
|
|
// Update total players count
|
|
document.getElementById('playerTreeCount').textContent = playersData.length;
|
|
|
|
filterAndSortPlayers();
|
|
} else {
|
|
console.error('Failed to load player stats:', result.message);
|
|
renderPlayersBasic();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading player stats:', error);
|
|
renderPlayersBasic();
|
|
}
|
|
}
|
|
|
|
// Setup event listeners
|
|
function setupEventListeners() {
|
|
// search handled inline via oninput on the input element
|
|
}
|
|
|
|
function setSort(sort) {
|
|
currentSort = sort;
|
|
document.querySelectorAll('.psort-btn').forEach(b =>
|
|
b.classList.toggle('active', b.dataset.sort === sort));
|
|
filterAndSortPlayers();
|
|
}
|
|
|
|
// Filter and sort players
|
|
function filterAndSortPlayers() {
|
|
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
|
|
|
|
// Sort full list and assign permanent ranks
|
|
const sorted = [...playersData].sort((a, b) => {
|
|
switch (currentSort) {
|
|
case 'best_score': return (b.stats?.best_tournament_score || 0) - (a.stats?.best_tournament_score || 0);
|
|
case 'total_tens': return (b.stats?.total_tens || 0) - (a.stats?.total_tens || 0);
|
|
case 'best_single': return (b.stats?.best_tournament_score || 0) - (a.stats?.best_tournament_score || 0);
|
|
default: return 0;
|
|
}
|
|
});
|
|
sorted.forEach((p, i) => { p._rank = i + 1; });
|
|
|
|
// Filter preserving original rank
|
|
filteredPlayers = sorted.filter(player => {
|
|
const matchesSearch = player.name.toLowerCase().includes(searchTerm);
|
|
const matchesFilter = currentFilter === 'all' ||
|
|
(currentFilter === 'current' && player.current_player) ||
|
|
(currentFilter === 'archived' && !player.current_player);
|
|
return matchesSearch && matchesFilter;
|
|
});
|
|
|
|
renderPlayers();
|
|
}
|
|
|
|
// Render players into the tree panel
|
|
let activePlayerId = null;
|
|
|
|
function renderPlayers() {
|
|
const treeList = document.getElementById('playerTreeList');
|
|
document.getElementById('playerTreeCount').textContent = filteredPlayers.length;
|
|
|
|
if (filteredPlayers.length === 0) {
|
|
treeList.innerHTML = `<div style="padding:24px;text-align:center;color:#bbb;font-size:0.82rem">Ni zadetkov</div>`;
|
|
return;
|
|
}
|
|
|
|
treeList.innerHTML = filteredPlayers.map((player) => {
|
|
const initials = player.name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
|
const active = player.id === activePlayerId ? ' active' : '';
|
|
let scoreVal, scoreMeta;
|
|
if (currentSort === 'total_tens') {
|
|
scoreVal = player.stats?.total_tens || 0;
|
|
scoreMeta = `${scoreVal} 🎯`;
|
|
} else {
|
|
scoreVal = player.stats?.best_tournament_score || 0;
|
|
scoreMeta = `${scoreVal} pts`;
|
|
}
|
|
const rank = player._rank || 0;
|
|
const rankHtml = rank === 1 ? `<div class="player-tree-rank">🥇</div>`
|
|
: rank === 2 ? `<div class="player-tree-rank">🥈</div>`
|
|
: rank === 3 ? `<div class="player-tree-rank">🥉</div>`
|
|
: `<div class="player-tree-rank-num">${rank}</div>`;
|
|
return `<div class="player-tree-item${active}" onclick="selectPlayer(${player.id})">
|
|
${rankHtml}
|
|
<div class="player-tree-info">
|
|
<div class="player-tree-name">${player.name}</div>
|
|
<div class="player-tree-meta">${player.stats?.total_tournaments || 0}T · ${scoreMeta}</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// Render players without stats (fallback)
|
|
function renderPlayersBasic() {
|
|
fetch('/api/players')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const basicPlayers = data.players || [];
|
|
playersData = basicPlayers.map(p => ({ id: p.id, name: p.name, current_player: p.enabled, stats: {} }));
|
|
filteredPlayers = [...playersData];
|
|
document.getElementById('playerTreeCount').textContent = playersData.length;
|
|
renderPlayers();
|
|
})
|
|
.catch(() => {
|
|
document.getElementById('playerTreeList').innerHTML =
|
|
`<div style="padding:24px;text-align:center;color:#bbb;font-size:0.82rem">Napaka pri nalaganju</div>`;
|
|
});
|
|
}
|
|
|
|
// Create mini performance chart for player
|
|
function createMiniChart(player) {
|
|
const canvas = document.getElementById(`chart-${player.id}`);
|
|
if (!canvas || !player.stats?.performance_trend) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: player.stats.performance_trend.map((_, i) => `T${i + 1}`),
|
|
datasets: [{
|
|
data: player.stats.performance_trend,
|
|
borderColor: player.current_player ? '#28a745' : '#6c757d',
|
|
backgroundColor: player.current_player ? 'rgba(40, 167, 69, 0.1)' : 'rgba(108, 117, 125, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 2,
|
|
pointHoverRadius: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false,
|
|
beginAtZero: true
|
|
}
|
|
},
|
|
elements: {
|
|
point: {
|
|
hoverBackgroundColor: player.current_player ? '#28a745' : '#6c757d'
|
|
}
|
|
},
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index'
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Select player — load stats into right panel iframe
|
|
function selectPlayer(playerId) {
|
|
activePlayerId = playerId;
|
|
renderPlayers();
|
|
|
|
document.getElementById('playerPlaceholder').style.display = 'none';
|
|
const embed = document.getElementById('playerDetailEmbed');
|
|
embed.style.display = 'block';
|
|
embed.innerHTML = `<iframe src="/archive/player/${playerId}"></iframe>`;
|
|
}
|
|
|
|
// Navigate to player stats (kept for compatibility)
|
|
function viewPlayerStats(playerId) {
|
|
selectPlayer(playerId);
|
|
}
|
|
|
|
// Initialize page when DOM is loaded and translations are ready
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
window.addEventListener('i18nReady', function() {
|
|
initializePage();
|
|
}, { once: true });
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |