Files
pdga-rating/public/js/players.js
T
2026-05-21 15:16:09 +02:00

551 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const cachedDebugInfo = {};
let pendingPlayerData = null;
let openPdgaNumber = null;
// ── Delta-pill helper ─────────────────────────────
function renderDeltaPill(value, extraClass) {
if (value == null) return null;
const cls = value > 0 ? 'up' : value < 0 ? 'down' : 'flat';
const glyph = value > 0 ? '▲' : value < 0 ? '▼' : '';
const num = value > 0 ? '+' + value : String(value);
return { glyph, num, cls };
}
function applyDeltaPill(pillEl, value) {
if (!pillEl || value == null) return;
const pill = renderDeltaPill(value);
pillEl.className = 'delta-pill ' + pill.cls;
while (pillEl.firstChild) pillEl.removeChild(pillEl.firstChild);
const glyphSpan = document.createElement('span');
glyphSpan.className = 'delta-glyph';
glyphSpan.textContent = pill.glyph;
const numSpan = document.createElement('span');
numSpan.className = 'delta-num';
numSpan.textContent = pill.num;
pillEl.appendChild(glyphSpan);
pillEl.appendChild(numSpan);
}
function setupTooltipsAfterSwap() {
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'ratings-table') {
initRatingsTooltips();
}
// After player history partial loads, render the chart
const target = event.detail.target;
if (target.id && target.id.startsWith('history-content-')) {
const container = target.querySelector('.player-chart, .chart-container');
if (container && container.dataset.history) {
try {
const history = JSON.parse(container.dataset.history);
createRatingChart(container, history);
} catch (e) {
console.error('Error rendering chart:', e);
}
}
}
});
}
function initRatingsTooltips() {
document.querySelectorAll('.predicted-value').forEach(span => {
const pdgaNumber = span.dataset.pdga;
const stdDev = span.dataset.stddev;
const tooltip = document.getElementById(`tooltip-stddev-${pdgaNumber}`);
if (stdDev && tooltip) {
setupTooltip(span, tooltip, () => `Standard Deviation: \u00b1${stdDev}`);
}
});
document.querySelectorAll('.rating-value').forEach(span => {
const pdgaNumber = span.dataset.pdga;
const rating = parseInt(span.dataset.rating);
const stdDev = parseInt(span.dataset.stddev);
const tooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && tooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
setupTooltip(span, tooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
});
}
function togglePlayerHistory(pdgaNumber) {
const historyRow = document.getElementById('history-' + pdgaNumber);
const contentDiv = document.getElementById('history-content-' + pdgaNumber);
const expandableRow = document.getElementById('row-' + pdgaNumber);
const isOpen = historyRow.style.display === 'table-row';
// Close any previously-open row
if (openPdgaNumber !== null && openPdgaNumber !== pdgaNumber) {
const prevHistory = document.getElementById('history-' + openPdgaNumber);
const prevRow = document.getElementById('row-' + openPdgaNumber);
if (prevHistory) {
prevHistory.style.display = 'none';
prevHistory.classList.remove('is-open');
}
if (prevRow) prevRow.classList.remove('row-open');
openPdgaNumber = null;
}
if (isOpen) {
historyRow.style.display = 'none';
historyRow.classList.remove('is-open');
expandableRow.classList.remove('row-open');
openPdgaNumber = null;
return;
}
historyRow.style.display = 'table-row';
// Force reflow so animation plays each open
historyRow.classList.remove('is-open');
void historyRow.offsetWidth;
historyRow.classList.add('is-open');
expandableRow.classList.add('row-open');
openPdgaNumber = pdgaNumber;
if (contentDiv.dataset.loaded === 'true') {
return;
}
htmx.ajax('GET', '/partials/player-history/' + pdgaNumber, {target: '#history-content-' + pdgaNumber, swap: 'innerHTML'});
contentDiv.dataset.loaded = 'true';
}
async function clearCache() {
try {
const response = await fetch('/api/clear-cache', { method: 'POST' });
const data = await response.json();
if (data.success) {
alert(data.message);
if (confirm('Reload page to fetch fresh data?')) {
location.reload();
}
} else {
alert('Failed to clear cache');
}
} catch (error) {
console.error('Error clearing cache:', error);
alert('Error clearing cache');
}
}
async function refreshPlayer(pdgaNumber) {
const icon = document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`);
if (icon) icon.classList.add('spinning');
try {
const response = await fetch(`/api/refresh-player/${pdgaNumber}`, { method: 'POST' });
const data = await response.json();
if (data.success) {
const row = document.getElementById(`row-${pdgaNumber}`);
const ratingCell = row.querySelector('.cell-rating');
const nameLink = row.querySelector('.player-name a');
if (nameLink) nameLink.textContent = data.player.name;
const ratingValue = ratingCell ? ratingCell.querySelector('.rating-value') : null;
if (ratingValue) {
ratingValue.textContent = data.player.rating || 'N/A';
ratingValue.dataset.rating = data.player.rating || '';
const stdDev = parseInt(ratingValue.dataset.stddev);
const rating = parseInt(data.player.rating);
const tooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && tooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
replaceWithTooltip(ratingValue, tooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
}
const deltaMonthPill = ratingCell ? ratingCell.querySelector('.delta-pill') : null;
applyDeltaPill(deltaMonthPill, data.player.ratingChange);
}
} catch (error) {
console.error('Error refreshing player:', error);
alert('Failed to refresh player data');
} finally {
if (icon) icon.classList.remove('spinning');
}
}
async function refreshRoundHistory(pdgaNumber) {
const icon = document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`);
if (icon) icon.classList.add('spinning');
try {
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, { method: 'POST' });
const data = await response.json();
if (!response.ok) {
throw new Error(JSON.stringify(data));
}
if (data.success) {
if (data.debugLog) {
cachedDebugInfo[pdgaNumber] = data.debugLog;
}
const predictedCell = document.getElementById(`predicted-${pdgaNumber}`);
if (predictedCell) {
const predictedValue = predictedCell.querySelector('.predicted-value');
if (predictedValue) {
predictedValue.textContent = data.predictedRating || 'N/A';
predictedValue.dataset.stddev = data.stdDev || '';
const tooltip = document.getElementById(`tooltip-stddev-${pdgaNumber}`);
if (data.stdDev && tooltip) {
replaceWithTooltip(predictedValue, tooltip, () => `Standard Deviation: \u00b1${data.stdDev}`);
}
}
}
const row = document.getElementById(`row-${pdgaNumber}`);
const ratingCell = row.querySelector('.cell-rating');
const ratingValue = ratingCell ? ratingCell.querySelector('.rating-value') : null;
if (ratingValue && data.stdDev) {
ratingValue.dataset.stddev = data.stdDev;
const rating = parseInt(ratingValue.dataset.rating);
const stdDev = parseInt(data.stdDev);
const ratingTooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && ratingTooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
replaceWithTooltip(ratingValue, ratingTooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
}
}
} catch (error) {
console.error('Error refreshing round history:', error);
let errorMessage = 'Failed to refresh prediction data';
let errorDetails = '';
try {
const errorData = JSON.parse(error.message);
errorMessage = errorData.error || errorMessage;
if (errorData.message) {
errorDetails = errorData.message;
} else {
errorDetails = errorData.details || '';
if (errorData.suggestion) {
errorDetails += '\n\nSuggestion: ' + errorData.suggestion;
}
if (errorData.errorType) {
errorDetails += '\n\nError Type: ' + errorData.errorType;
}
if (errorData.timestamp) {
errorDetails += '\n\nTime: ' + new Date(errorData.timestamp).toLocaleString();
}
}
} catch (parseError) {
errorDetails = error.message;
}
const fullMessage = errorDetails ? errorMessage + '\n\n' + errorDetails : errorMessage;
alert(fullMessage);
} finally {
if (icon) icon.classList.remove('spinning');
}
}
async function refreshRatingHistory(pdgaNumber) {
// No dedicated icon in the expanded row; spinner state not needed here
const icon = null;
try {
const response = await fetch(`/api/refresh-rating-history/${pdgaNumber}`, { method: 'POST' });
const data = await response.json();
if (data.success) {
const contentDiv = document.getElementById(`history-content-${pdgaNumber}`);
contentDiv.dataset.loaded = 'false';
htmx.ajax('GET', `/partials/player-history/${pdgaNumber}`, {target: `#history-content-${pdgaNumber}`, swap: 'innerHTML'});
contentDiv.dataset.loaded = 'true';
}
} catch (error) {
console.error('Error refreshing rating history:', error);
alert('Failed to refresh rating history');
}
}
async function showDebugInfo(pdgaNumber) {
const modal = document.getElementById('debug-modal');
const header = document.getElementById('debug-header');
const log = document.getElementById('debug-log');
const playerNameElement = document.querySelector(`#row-${pdgaNumber} .player-name a`);
const playerName = playerNameElement ? playerNameElement.textContent : `PDGA #${pdgaNumber}`;
header.textContent = `Prediction Calculation Details - ${playerName}`;
log.textContent = 'Loading calculation details...';
modal.style.display = 'flex';
try {
if (cachedDebugInfo[pdgaNumber]) {
log.textContent = cachedDebugInfo[pdgaNumber].join('\n');
return;
}
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, { method: 'POST' });
const data = await response.json();
if (data.success && data.debugLog) {
cachedDebugInfo[pdgaNumber] = data.debugLog;
log.textContent = data.debugLog.join('\n');
} else {
log.textContent = 'No debug information available. Try refreshing the prediction first.';
}
} catch (error) {
console.error('Error fetching debug info:', error);
log.textContent = 'Error loading debug information. Please try again.';
}
}
function closeDebugModal(event) {
document.getElementById('debug-modal').style.display = 'none';
}
async function searchAndAddPlayer(event) {
if (event) event.preventDefault();
const input = document.getElementById('pdga-number-input');
const pdgaNumber = input.value.trim();
if (!pdgaNumber) {
alert('Please enter a PDGA number');
return;
}
const button = document.querySelector('.add-bar button[type="submit"]');
const originalText = button ? button.textContent : '';
if (button) { button.disabled = true; button.textContent = 'Searching...'; }
try {
const response = await fetch(`/api/search-player/${pdgaNumber}`);
const data = await response.json();
if (!response.ok) {
showErrorModal(data.error || 'Player not found');
return;
}
if (data.alreadyExists) {
showInfoModal(`${data.player.name} is already being tracked!`);
return;
}
pendingPlayerData = data.player;
showConfirmationModal(data.player);
} catch (error) {
console.error('Error searching for player:', error);
showErrorModal('Failed to search for player. Please try again.');
} finally {
if (button) { button.disabled = false; button.textContent = originalText; }
}
}
function showConfirmationModal(player) {
const modal = document.getElementById('add-player-modal');
document.getElementById('add-player-modal-header').textContent = 'Confirm Player';
const body = document.getElementById('add-player-modal-body');
body.textContent = '';
const question = document.createElement('p');
question.textContent = 'Is this the correct player you want to add?';
body.appendChild(question);
const info = document.createElement('div');
info.style.cssText = 'background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 15px;';
const name = document.createElement('strong');
name.style.cssText = 'font-size: 18px; color: #007bff;';
name.textContent = player.name;
info.appendChild(name);
info.appendChild(document.createElement('br'));
const pdga = document.createElement('span');
pdga.style.color = '#6c757d';
pdga.textContent = `PDGA #${player.pdgaNumber}`;
info.appendChild(pdga);
info.appendChild(document.createElement('br'));
const rating = document.createElement('span');
if (player.rating) {
rating.style.cssText = 'color: #28a745; font-weight: bold;';
rating.textContent = `Current Rating: ${player.rating}`;
} else {
rating.style.color = '#999';
rating.textContent = 'No rating available';
}
info.appendChild(rating);
body.appendChild(info);
const footer = document.getElementById('add-player-modal-footer');
footer.textContent = '';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'btn btn-cancel';
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = closeAddPlayerModal;
footer.appendChild(cancelBtn);
const confirmBtn = document.createElement('button');
confirmBtn.className = 'btn btn-confirm';
confirmBtn.textContent = 'Add Player';
confirmBtn.onclick = confirmAddPlayer;
footer.appendChild(confirmBtn);
modal.style.display = 'flex';
}
function showErrorModal(message) {
const modal = document.getElementById('add-player-modal');
document.getElementById('add-player-modal-header').textContent = 'Player Not Found';
const body = document.getElementById('add-player-modal-body');
body.textContent = '';
const errorP = document.createElement('p');
errorP.style.color = '#dc3545';
errorP.textContent = message;
body.appendChild(errorP);
const helpP = document.createElement('p');
helpP.style.cssText = 'margin-top: 10px; color: #6c757d; font-size: 14px;';
helpP.textContent = 'Please check the PDGA number and try again.';
body.appendChild(helpP);
const footer = document.getElementById('add-player-modal-footer');
footer.textContent = '';
const closeBtn = document.createElement('button');
closeBtn.className = 'btn btn-cancel';
closeBtn.textContent = 'Close';
closeBtn.onclick = closeAddPlayerModal;
footer.appendChild(closeBtn);
modal.style.display = 'flex';
}
function showInfoModal(message) {
const modal = document.getElementById('add-player-modal');
document.getElementById('add-player-modal-header').textContent = 'Information';
const body = document.getElementById('add-player-modal-body');
body.textContent = '';
const infoP = document.createElement('p');
infoP.style.color = '#007bff';
infoP.textContent = message;
body.appendChild(infoP);
const footer = document.getElementById('add-player-modal-footer');
footer.textContent = '';
const closeBtn = document.createElement('button');
closeBtn.className = 'btn btn-cancel';
closeBtn.textContent = 'Close';
closeBtn.onclick = closeAddPlayerModal;
footer.appendChild(closeBtn);
modal.style.display = 'flex';
}
async function confirmAddPlayer() {
if (!pendingPlayerData) {
closeAddPlayerModal();
return;
}
const body = document.getElementById('add-player-modal-body');
body.textContent = 'Adding player...';
document.getElementById('add-player-modal-footer').textContent = '';
try {
const response = await fetch('/api/add-player', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pdgaNumber: pendingPlayerData.pdgaNumber })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to add player');
}
body.textContent = '';
const successP = document.createElement('p');
successP.style.cssText = 'color: #28a745; text-align: center;';
successP.textContent = `${data.player.name} has been added successfully!`;
body.appendChild(successP);
const footer = document.getElementById('add-player-modal-footer');
footer.textContent = '';
const okBtn = document.createElement('button');
okBtn.className = 'btn btn-confirm';
okBtn.textContent = 'OK';
okBtn.onclick = function() { closeAddPlayerModal(); location.reload(); };
footer.appendChild(okBtn);
document.getElementById('pdga-number-input').value = '';
pendingPlayerData = null;
} catch (error) {
console.error('Error adding player:', error);
body.textContent = error.message;
body.style.color = '#dc3545';
const footer = document.getElementById('add-player-modal-footer');
footer.textContent = '';
const closeBtn = document.createElement('button');
closeBtn.className = 'btn btn-cancel';
closeBtn.textContent = 'Close';
closeBtn.onclick = closeAddPlayerModal;
footer.appendChild(closeBtn);
}
}
function closeAddPlayerModal(event) {
document.getElementById('add-player-modal').style.display = 'none';
pendingPlayerData = null;
}
// ── Sparkline toggle ───────────────────────────────
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('trendchart-toggle');
if (!btn) return;
const state = localStorage.getItem('ratingtracker.sparklines') || 'on';
document.body.dataset.sparklines = state;
btn.setAttribute('aria-pressed', state === 'on' ? 'true' : 'false');
btn.addEventListener('click', function() {
const next = document.body.dataset.sparklines === 'on' ? 'off' : 'on';
document.body.dataset.sparklines = next;
btn.setAttribute('aria-pressed', next === 'on' ? 'true' : 'false');
localStorage.setItem('ratingtracker.sparklines', next);
});
});
// ── Expandable row keyboard support ───────────────
document.addEventListener('keydown', function(e) {
if (e.key !== 'Enter' && e.key !== ' ') return;
const row = e.target;
if (!row.classList || !row.classList.contains('expandable-row')) return;
e.preventDefault();
const pdgaNumber = row.id.replace('row-', '');
togglePlayerHistory(parseInt(pdgaNumber, 10));
});