diff --git a/app.js b/app.js index d34fbad..e844d90 100644 --- a/app.js +++ b/app.js @@ -302,8 +302,6 @@ async function saveMatch(match, playerEntries) { return; } - persistPlayers(); - persistMatches(); toggleLoading(true); try { @@ -312,12 +310,17 @@ async function saveMatch(match, playerEntries) { } await createMatch(match); + persistPlayers(); + persistMatches(); + const gameType = match.type === 'singles' ? 'Einzel' : 'Doppel'; showSuccess(`π ${gameType}-Match gespeichert! ${match.winnerName} gewinnt gegen ${match.loserName} (+${match.eloChange} Elo)`); showConfetti(); } catch (err) { console.error(err); - showError('Match lokal gespeichert, aber nicht mit Supabase synchronisiert.'); + state.matches.pop(); + recalculateStatsFromHistory(); + showError('Fehler beim Speichern. Match wurde nicht ΓΌbertragen.'); } finally { toggleLoading(false); renderRankings(openProfileModal); @@ -332,6 +335,7 @@ async function removeMatch(id) { toggleLoading(true); try { + const match = state.matches.find(m => m.id === id); await deleteMatch(id); state.matches = state.matches.filter(m => m.id !== id); @@ -339,12 +343,13 @@ async function removeMatch(id) { recalculateStatsFromHistory(); - // Alle Spieler-ELOs in Supabase aktualisieren - await Promise.all( - Object.entries(state.players).map(([playerId, player]) => - updatePlayer(playerId, player) - ) - ); + // Only write back players who appeared in the deleted match + const affectedIds = match + ? [...String(match.winnerId || '').split(','), ...String(match.loserId || '').split(',')] + .map(s => s.trim()).filter(s => s && state.players[s]) + : Object.keys(state.players); + + await Promise.all(affectedIds.map(pid => updatePlayer(pid, state.players[pid]))); persistPlayers(); renderRankings(openProfileModal); diff --git a/src/branding.js b/src/branding.js index 738cf8e..1774998 100644 --- a/src/branding.js +++ b/src/branding.js @@ -47,7 +47,16 @@ export function applyBranding(branding = {}) { // ββ Hilfsfunktionen ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +function expandHex(hex) { + // Expand 3-digit shorthand (#f00 β #ff0000) + if (/^#[0-9a-fA-F]{3}$/.test(hex)) { + return '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]; + } + return hex; +} + function hexToRgba(hex, alpha) { + hex = expandHex(hex); const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); @@ -55,6 +64,7 @@ function hexToRgba(hex, alpha) { } function darken(hex, amount) { + hex = expandHex(hex); const r = Math.max(0, Math.round(parseInt(hex.slice(1, 3), 16) * (1 - amount))); const g = Math.max(0, Math.round(parseInt(hex.slice(3, 5), 16) * (1 - amount))); const b = Math.max(0, Math.round(parseInt(hex.slice(5, 7), 16) * (1 - amount))); diff --git a/src/chart.js b/src/chart.js index e8f24ae..ca2b539 100644 --- a/src/chart.js +++ b/src/chart.js @@ -1,5 +1,5 @@ import Chart from 'https://cdn.jsdelivr.net/npm/chart.js@4/+esm'; -import { state } from './state.js'; +import { state, normaliseMatch } from './state.js'; import { STARTING_ELO, calculateSinglesMatch, calculateDoublesMatch } from './elo.js'; // ββ Farbpalette ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ @@ -31,15 +31,19 @@ export function buildEloHistory(type = 'singles') { const getIds = (val) => String(val || '').split(',').map(s => s.trim()).filter(Boolean); const sorted = [...state.matches] - .filter(m => String(m.type || '').toLowerCase() === type) .sort((a, b) => new Date(a.date || 0) - new Date(b.date || 0)); let matchIndex = 1; sorted.forEach(match => { + const { type: matchType, winnerId: rawWId, loserId: rawLId } = normaliseMatch(match); + const isDoubles = matchType.includes('doubles') || String(rawWId || '').includes(','); + if (type === 'singles' && isDoubles) return; + if (type === 'doubles' && !isDoubles) return; + if (type === 'singles') { - const wId = String(match.winnerId || '').trim(); - const lId = String(match.loserId || '').trim(); + const wId = String(rawWId || '').trim(); + const lId = String(rawLId || '').trim(); if (currentElo[wId] === undefined || currentElo[lId] === undefined) return; const result = calculateSinglesMatch(currentElo[wId], currentElo[lId]); @@ -49,8 +53,8 @@ export function buildEloHistory(type = 'singles') { history[wId].push({ matchIndex, date: match.date, elo: result.winnerElo }); history[lId].push({ matchIndex, date: match.date, elo: result.loserElo }); } else { - const winners = getIds(match.winnerId); - const losers = getIds(match.loserId); + const winners = getIds(rawWId); + const losers = getIds(rawLId); if (winners.some(id => currentElo[id] === undefined) || losers.some(id => currentElo[id] === undefined)) return; @@ -98,11 +102,21 @@ export function renderPlayerChart(playerId, type = 'singles') { const points = history[playerId] || []; if (points.length === 0) { - canvas.parentElement.innerHTML = - '
Noch keine Spiele in diesem Modus.
'; + let msg = canvas.parentElement.querySelector('.chart-empty-msg'); + if (!msg) { + msg = document.createElement('p'); + msg.className = 'chart-empty-msg'; + msg.style.cssText = 'text-align:center;color:#999;padding:20px;margin:0'; + canvas.parentElement.appendChild(msg); + } + msg.textContent = 'Noch keine Spiele in diesem Modus.'; + canvas.style.display = 'none'; return; } + canvas.style.display = ''; + canvas.parentElement.querySelector('.chart-empty-msg')?.remove(); + const color = '#c51216'; const data = [ { x: 0, y: STARTING_ELO, date: null }, @@ -170,11 +184,21 @@ export function renderEloChart(type = 'singles') { .sort((a, b) => a[1].name.localeCompare(b[1].name)); if (activePlayers.length === 0) { - canvas.parentElement.innerHTML = - 'Noch keine Matches eingetragen.
'; + let msg = canvas.parentElement.querySelector('.chart-empty-msg'); + if (!msg) { + msg = document.createElement('p'); + msg.className = 'chart-empty-msg'; + msg.style.cssText = 'text-align:center;color:#999;padding:40px;margin:0'; + canvas.parentElement.appendChild(msg); + } + msg.textContent = 'Noch keine Matches eingetragen.'; + canvas.style.display = 'none'; return; } + canvas.style.display = ''; + canvas.parentElement.querySelector('.chart-empty-msg')?.remove(); + const datasets = activePlayers.map(([id, player], index) => { const color = colorFor(index); const points = history[id]; diff --git a/src/state.js b/src/state.js index fc4a32f..72d177d 100644 --- a/src/state.js +++ b/src/state.js @@ -1,5 +1,24 @@ import { STARTING_ELO, calculateSinglesMatch, calculateDoublesMatch } from './elo.js'; +// ββ Match-Normalisierung βββββββββββββββββββββββββββββββββββββββββββββββββββ + +/** + * Normalises a raw match object, correcting the column-shift bug present in + * data imported from old Google Sheets exports where winnerId held the type. + * Returns { type, winnerId, loserId } with correct values. + */ +export function normaliseMatch(match) { + const rawType = String(match.type || '').toLowerCase(); + const rawWinnerId = String(match.winnerId || '').toLowerCase(); + + const columnShifted = rawWinnerId.includes('doubles') || rawWinnerId.includes('singles'); + const type = columnShifted ? rawWinnerId : rawType; + const winnerId = columnShifted ? match.loserId : match.winnerId; + const loserId = columnShifted ? match.winnerName : match.loserId; + + return { type, winnerId, loserId }; +} + // ================= APP-ZUSTAND ================= export const state = { @@ -66,16 +85,8 @@ export function recalculateStatsFromHistory() { let skipped = 0; sorted.forEach(match => { - const rawType = String(match.type || '').toLowerCase(); - const rawWinnerId = String(match.winnerId || '').toLowerCase(); - - // Workaround: Γ€ltere Matches aus Google Sheets hatten verschobene Spalten - const columnShifted = rawWinnerId.includes('doubles') || rawWinnerId.includes('singles'); - const actualType = columnShifted ? rawWinnerId : rawType; - const isDoubles = actualType.includes('doubles') || String(match.winnerId).includes(','); - - const wRaw = columnShifted ? match.loserId : match.winnerId; - const lRaw = columnShifted ? match.winnerName : match.loserId; + const { type: actualType, winnerId: wRaw, loserId: lRaw } = normaliseMatch(match); + const isDoubles = actualType.includes('doubles') || String(wRaw || '').includes(','); if (!isDoubles) { const winnerId = String(wRaw || '').trim(); diff --git a/src/ui.js b/src/ui.js index 626d0c3..1f20435 100644 --- a/src/ui.js +++ b/src/ui.js @@ -172,7 +172,7 @@ export function showRankingTab(type) { }); document.getElementById('singles-ranking').style.display = type === 'singles' ? 'block' : 'none'; document.getElementById('doubles-ranking').style.display = type === 'doubles' ? 'block' : 'none'; - renderRankings(); + renderRankings(_onPlayerRowClick); } let _onPlayerRowClick = null;