Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,6 @@ async function saveMatch(match, playerEntries) {
return;
}

persistPlayers();
persistMatches();
toggleLoading(true);

try {
Expand All @@ -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);
Expand All @@ -332,19 +335,21 @@ 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);
persistMatches();

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);
Expand Down
10 changes: 10 additions & 0 deletions src/branding.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,24 @@ 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);
return `rgba(${r}, ${g}, ${b}, ${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)));
Expand Down
44 changes: 34 additions & 10 deletions src/chart.js
Original file line number Diff line number Diff line change
@@ -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 ────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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]);
Expand All @@ -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;

Expand Down Expand Up @@ -98,11 +102,21 @@ export function renderPlayerChart(playerId, type = 'singles') {
const points = history[playerId] || [];

if (points.length === 0) {
canvas.parentElement.innerHTML =
'<p style="text-align:center;color:#999;padding:20px">Noch keine Spiele in diesem Modus.</p>';
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 },
Expand Down Expand Up @@ -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 =
'<p style="text-align:center;color:#999;padding:40px">Noch keine Matches eingetragen.</p>';
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];
Expand Down
31 changes: 21 additions & 10 deletions src/state.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading