3dcd3131a0
Add getMonthlyHistory() to models/player for single-player use and getAllMonthlyHistoriesFromDB() for bulk fetches (one query, grouped in memory). Wire monthlyHistory into all player objects returned by getPlayerDataFromDB and getAllRatingsFromDB. Bulk path pre-fetches in one query to avoid N extra per-player queries.
483 lines
17 KiB
JavaScript
483 lines
17 KiB
JavaScript
let cachedDebugInfo = {};
|
|
let pendingPlayerData = null;
|
|
|
|
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('.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}`);
|
|
|
|
if (historyRow.style.display === 'table-row') {
|
|
historyRow.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
historyRow.style.display = 'table-row';
|
|
|
|
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} .rating .refresh-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('.rating');
|
|
|
|
const nameLink = row.querySelector('.player-name a');
|
|
nameLink.textContent = data.player.name;
|
|
|
|
const ratingValue = ratingCell.querySelector('.rating-value');
|
|
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.querySelector('.delta-pill');
|
|
if (deltaMonthPill && data.player.ratingChange != null) {
|
|
const pillChange = data.player.ratingChange;
|
|
const pillText = pillChange > 0 ? `+${pillChange}` : pillChange.toString();
|
|
const pillClass = pillChange > 0 ? 'up' : pillChange < 0 ? 'down' : 'flat';
|
|
deltaMonthPill.textContent = pillText;
|
|
deltaMonthPill.className = `delta-pill ${pillClass}`;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error refreshing player:', error);
|
|
alert('Failed to refresh player data');
|
|
} finally {
|
|
icon.classList.remove('spinning');
|
|
}
|
|
}
|
|
|
|
async function refreshRoundHistory(pdgaNumber) {
|
|
const icon = document.querySelector(`#predicted-${pdgaNumber} .refresh-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('.rating');
|
|
const ratingValue = ratingCell.querySelector('.rating-value');
|
|
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 {
|
|
icon.classList.remove('spinning');
|
|
}
|
|
}
|
|
|
|
async function refreshRatingHistory(pdgaNumber) {
|
|
const icon = document.querySelector(`#history-${pdgaNumber} .chart-title .refresh-icon`);
|
|
icon.classList.add('spinning');
|
|
|
|
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');
|
|
} finally {
|
|
icon.classList.remove('spinning');
|
|
}
|
|
}
|
|
|
|
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() {
|
|
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('.btn-add');
|
|
const originalText = button.textContent;
|
|
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 {
|
|
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;
|
|
}
|