664 lines
25 KiB
JavaScript
664 lines
25 KiB
JavaScript
const cachedDebugInfo = {};
|
||
let pendingPlayerData = null;
|
||
let openPdgaNumber = null;
|
||
|
||
// ── Delta-pill helper ─────────────────────────────
|
||
function renderDeltaPill(value, extraClass) {
|
||
const isNull = (value == null);
|
||
const cls = isNull ? 'flat' : value > 0 ? 'up' : value < 0 ? 'down' : 'flat';
|
||
const glyph = (isNull || value === 0) ? '–' : value > 0 ? '▲' : '▼';
|
||
const num = isNull ? '—' : value > 0 ? '+' + value : String(value);
|
||
return { glyph, num, cls };
|
||
}
|
||
|
||
function applyDeltaPill(pillEl, value) {
|
||
if (!pillEl) 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 initChartsIn(rootEl) {
|
||
rootEl.querySelectorAll('.player-chart').forEach(function(container) {
|
||
if (container.dataset.charted === 'true') return;
|
||
if (!container.dataset.history) return;
|
||
try {
|
||
const history = JSON.parse(container.dataset.history);
|
||
createRatingChart(container, history);
|
||
container.dataset.charted = 'true';
|
||
} catch (e) {
|
||
console.error('Error rendering chart:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
function setupAfterTableSwap() {
|
||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||
const target = event.detail.target;
|
||
if (target.id === 'ratings-table') {
|
||
initRatingsTooltips();
|
||
initChartsIn(target);
|
||
return;
|
||
}
|
||
if (target.id && target.id.startsWith('history-content-')) {
|
||
initChartsIn(target);
|
||
}
|
||
});
|
||
}
|
||
|
||
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');
|
||
}
|
||
}
|
||
|
||
// Refreshes both the current rating and the prediction in one click, then
|
||
// re-swaps the table so every derived value (deltas, pills, sparkline) reflects
|
||
// the new state. Cheaper than fine-grained DOM updates and guaranteed consistent
|
||
// because the server renders the truth.
|
||
async function refreshPlayerData(pdgaNumber) {
|
||
const icon = document.querySelector(`#row-${pdgaNumber} .cell-actions .refresh-icon`);
|
||
if (icon) icon.classList.add('spinning');
|
||
try {
|
||
await Promise.allSettled([
|
||
fetch(`/api/refresh-player/${pdgaNumber}`, { method: 'POST' }),
|
||
fetch(`/api/refresh-round-history/${pdgaNumber}`, { method: 'POST' })
|
||
]);
|
||
htmx.ajax('GET', '/partials/ratings-table', { target: '#ratings-table', swap: 'innerHTML' });
|
||
} catch (error) {
|
||
console.error('Error refreshing player data:', error);
|
||
} finally {
|
||
if (icon) icon.classList.remove('spinning');
|
||
}
|
||
}
|
||
|
||
async function refreshPlayer(pdgaNumber) {
|
||
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');
|
||
}
|
||
}
|
||
|
||
async function refreshRoundHistory(pdgaNumber) {
|
||
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) {
|
||
// Rate-limited or scrape failure — common when refresh runs alongside the
|
||
// current-rating refresh. Log but don't alert; the user-facing surface is
|
||
// the spinner stopping (data may or may not have updated).
|
||
console.error('Error refreshing round history:', error);
|
||
}
|
||
}
|
||
|
||
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));
|
||
});
|
||
|
||
// ── Target Rating Calculator ───────────────────────
|
||
function openTargetRatingModal(pdgaNumber) {
|
||
const modal = document.getElementById('target-rating-modal');
|
||
const header = document.getElementById('target-rating-modal-header');
|
||
const pdgaField = document.getElementById('target-rating-pdga');
|
||
const result = document.getElementById('target-rating-result');
|
||
const targetInput = document.getElementById('target-rating-input');
|
||
const roundsInput = document.getElementById('target-rounds-input');
|
||
const submitBtn = document.getElementById('target-rating-submit');
|
||
|
||
const playerNameEl = document.querySelector('#row-' + pdgaNumber + ' .player-name a');
|
||
const playerName = playerNameEl ? playerNameEl.textContent : 'PDGA #' + pdgaNumber;
|
||
|
||
header.textContent = 'Calculate Target Rating — ' + playerName;
|
||
pdgaField.value = pdgaNumber;
|
||
result.style.display = 'none';
|
||
while (result.firstChild) result.removeChild(result.firstChild);
|
||
targetInput.value = '';
|
||
roundsInput.value = '4';
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = 'Calculate';
|
||
modal.style.display = 'flex';
|
||
targetInput.focus();
|
||
}
|
||
|
||
function _targetResultMsg(parent, cls, text) {
|
||
const d = document.createElement('div');
|
||
d.className = cls;
|
||
d.textContent = text;
|
||
parent.appendChild(d);
|
||
}
|
||
|
||
async function calculateTargetRating(event) {
|
||
if (event) event.preventDefault();
|
||
const pdgaNumber = document.getElementById('target-rating-pdga').value;
|
||
const targetRating = parseInt(document.getElementById('target-rating-input').value, 10);
|
||
const rounds = parseInt(document.getElementById('target-rounds-input').value, 10);
|
||
const result = document.getElementById('target-rating-result');
|
||
const submitBtn = document.getElementById('target-rating-submit');
|
||
|
||
function clearResult() {
|
||
while (result.firstChild) result.removeChild(result.firstChild);
|
||
}
|
||
|
||
if (!Number.isInteger(targetRating) || targetRating < 400 || targetRating > 1200) {
|
||
result.style.display = 'block';
|
||
clearResult();
|
||
_targetResultMsg(result, 'error', 'Target rating must be 400-1200.');
|
||
return;
|
||
}
|
||
if (!Number.isInteger(rounds) || rounds < 1 || rounds > 20) {
|
||
result.style.display = 'block';
|
||
clearResult();
|
||
_targetResultMsg(result, 'error', 'Rounds must be an integer 1-20.');
|
||
return;
|
||
}
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = 'Calculating...';
|
||
result.style.display = 'block';
|
||
clearResult();
|
||
_targetResultMsg(result, 'loading', 'Calculating...');
|
||
|
||
try {
|
||
const response = await fetch('/api/calculate-target-rating/' + pdgaNumber, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ targetRating: targetRating, rounds: rounds })
|
||
});
|
||
const data = await response.json();
|
||
|
||
clearResult();
|
||
|
||
if (!response.ok || !data.success) {
|
||
const msg = data.details ? data.error + ': ' + data.details : (data.error || 'Calculation failed');
|
||
_targetResultMsg(result, 'error', msg);
|
||
return;
|
||
}
|
||
|
||
const summary = document.createElement('div');
|
||
summary.className = 'target-summary';
|
||
|
||
const avgLine = document.createElement('div');
|
||
const avgStrong = document.createElement('strong');
|
||
avgStrong.textContent = 'Required round average: ';
|
||
avgLine.appendChild(avgStrong);
|
||
avgLine.appendChild(document.createTextNode(String(data.requiredAverage)));
|
||
summary.appendChild(avgLine);
|
||
|
||
const currentLine = document.createElement('div');
|
||
currentLine.textContent = 'Current predicted rating: ' + data.currentRating;
|
||
summary.appendChild(currentLine);
|
||
|
||
const simLine = document.createElement('div');
|
||
simLine.textContent = 'Simulated predicted rating at this average: ' + data.predictedRating;
|
||
summary.appendChild(simLine);
|
||
|
||
const mutedLine = document.createElement('div');
|
||
mutedLine.className = 'muted';
|
||
mutedLine.textContent = 'Across ' + data.rounds + ' synthetic rounds before the next PDGA update.';
|
||
summary.appendChild(mutedLine);
|
||
|
||
if (data.warning) {
|
||
_targetResultMsg(summary, 'warning', data.warning);
|
||
}
|
||
|
||
result.appendChild(summary);
|
||
} catch (err) {
|
||
console.error('Error calculating target rating:', err);
|
||
clearResult();
|
||
_targetResultMsg(result, 'error', 'Network error. Please try again.');
|
||
} finally {
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = 'Calculate';
|
||
}
|
||
}
|
||
|
||
function closeTargetRatingModal(event) {
|
||
document.getElementById('target-rating-modal').style.display = 'none';
|
||
}
|