Laden...
Voorspelde groepsranglijsten
`; GROUPS.forEach(g => { const actual = actualRankings[g.name]; html += `
${g.name}
${allUsers.map(u => { const fn = u.first_name || cap(u.username); const dup = allUsers.some(x => x.username !== u.username && x.first_name === u.first_name); const nm = dup && u.last_name ? fn + ' ' + u.last_name.charAt(0).toUpperCase() + '.' : fn; return ''; }).join('')} `; for(let pos=0; pos<4; pos++){ html += '' + '' + allUsers.map(u => { const r = ranks.find(r => r.username===u.username && r.group_name===g.name && r.position===pos); let bg = ''; if(actual && actual[pos] && r && r.team === actual[pos]) bg = 'background:#d1fae5;font-weight:800;'; return ''; }).join('') + ''; } html += `
Positie
' + nm + '
' + (pos+1) + 'e plaats' + (r&&r.team||'–') + '
`; }); html += ``; // Bonus view (hidden by default) html += ``; // Knockout view (hidden by default) html += ``; return html; } function filterOvz(view){ ['matches','rankings','bonus','knockout'].forEach(v => { const el = document.getElementById('ovz-'+v); const btn = document.getElementById('ovz-btn-'+v); if(el) el.style.display = v===view ? 'block' : 'none'; if(btn){ btn.className = v===view ? 'btn-sm btn-orange' : 'btn-sm btn-outline'; } }); } async function exportAllData(){ const btn = event.target; btn.textContent = '⏳ Exporteren...'; btn.disabled = true; try { const [users, preds, ranks, bonus, koPreds] = await Promise.all([ sb.from('users').select('*').eq('is_admin', false).order('username'), sb.from('predictions').select('*'), sb.from('rankings').select('*'), sb.from('bonus').select('*'), sb.from('knockout_predictions').select('*'), ]); const userList = (users.data||[]); const usernames = userList.map(u => u.username); // ── Helper: naam weergave ── function naam(username){ const u = userList.find(x => x.username === username); return u ? ((u.first_name||'') + ' ' + (u.last_name||'')).trim() || username : username; } // ── Sheet 1: Groepswedstrijden ── const matchRows = [['Groep','Datum','Tijd','Thuis','Uit', ...usernames.map(naam)]]; GROUPS.forEach(g => { g.matches.forEach(m => { const row = [g.name, m.date, m.time, m.home, m.away]; usernames.forEach(u => { const p = (preds.data||[]).find(x => x.username===u && x.match_id===m.id); row.push(p && p.home_score!==null && p.away_score!==null ? p.home_score+'-'+p.away_score : ''); }); matchRows.push(row); }); }); // ── Sheet 2: Ranglijsten ── const rankRows = [['Groep','Positie', ...usernames.map(naam)]]; GROUPS.forEach(g => { for(let pos=0; pos<4; pos++){ const row = [g.name, pos+1+'e']; usernames.forEach(u => { const r = (ranks.data||[]).find(x => x.username===u && x.group_name===g.name && x.position===pos); row.push(r ? (r.team||'') : ''); }); rankRows.push(row); } }); // ── Sheet 3: Bonusvragen ── const bonusRows = [['Vraag','Punten', ...usernames.map(naam)]]; BONUS_QS.forEach(q => { const row = [q.label, q.pts+'pt']; usernames.forEach(u => { const b = (bonus.data||[]).find(x => x.username===u && x.question_id===q.id); row.push(b ? (b.answer||'') : ''); }); bonusRows.push(row); }); // ── Sheet 4: Knock-out ── const koRows = [['Ronde','Datum','Tijd','Wedstrijd', ...usernames.map(naam)]]; KNOCKOUT_ROUNDS.forEach(round => { round.matches.forEach(m => { const row = [round.name, m.date, m.time, m.label]; usernames.forEach(u => { const k = (koPreds.data||[]).find(x => x.username===u && x.match_id===m.id); row.push(k && k.home_score!==null && k.away_score!==null ? k.home_score+'-'+k.away_score : ''); }); koRows.push(row); }); }); // ── Sheet 5: Deelnemers ── const userRows = [['Gebruikersnaam','Voornaam','Achternaam','Email','Betaald']]; userList.forEach(u => { userRows.push([u.username, u.first_name||'', u.last_name||'', u.email||'', u.paid ? 'Ja' : 'Nee']); }); // ── Bouw Excel met SheetJS ── const XLSX = await import('https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs'); const wb = XLSX.utils.book_new(); function addSheet(name, rows){ const ws = XLSX.utils.aoa_to_sheet(rows); // Kolombreedtes ws['!cols'] = rows[0].map((_,i) => ({wch: i < 4 ? 20 : 14})); XLSX.utils.book_append_sheet(wb, ws, name); } addSheet('Groepswedstrijden', matchRows); addSheet('Ranglijsten', rankRows); addSheet('Bonusvragen', bonusRows); addSheet('Knock-out', koRows); addSheet('Deelnemers', userRows); XLSX.writeFile(wb, 'schaik_poule_export.xlsx'); btn.textContent = '✓ Geëxporteerd!'; setTimeout(() => { btn.textContent = '⬇ Export naar Excel'; btn.disabled = false; }, 2000); } catch(e) { console.error('Export fout:', e); btn.textContent = '⚠ Fout — probeer opnieuw'; btn.disabled = false; } } async function syncResultsFromAPI(){ const syncBtn = document.getElementById('sync-btn'); if(syncBtn){ syncBtn.textContent = '⏳ Bezig...'; syncBtn.disabled = true; } try { const url = 'https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json'; const resp = await fetch(url); if(!resp.ok) throw new Error('API niet bereikbaar'); const data = await resp.json(); // Map team names from English to Dutch const nameMap = { 'Mexico': 'Mexico', 'South Africa': 'Zuid-Afrika', 'South Korea': 'Zuid-Korea', 'Czech Republic': 'Tsjechië', 'Canada': 'Canada', 'Bosnia-Herzegovina': 'Bosnië', 'Qatar': 'Qatar', 'Switzerland': 'Zwitserland', 'Brazil': 'Brazilië', 'Morocco': 'Marokko', 'Haiti': 'Haïti', 'Scotland': 'Schotland', 'USA': 'USA', 'Paraguay': 'Paraguay', 'Australia': 'Australië', 'Turkey': 'Turkije', 'Germany': 'Duitsland', 'Curaçao': 'Curaçao', "Côte d'Ivoire": 'Ivoorkust', 'Ecuador': 'Ecuador', 'Netherlands': 'Nederland', 'Japan': 'Japan', 'Sweden': 'Zweden', 'Tunisia': 'Tunesië', 'Belgium': 'België', 'Egypt': 'Egypte', 'Iran': 'Iran', 'New Zealand': 'Nieuw-Zeeland', 'Spain': 'Spanje', 'Cape Verde': 'Kaapverdië', 'Saudi Arabia': 'Saudi-Arabië', 'Uruguay': 'Uruguay', 'France': 'Frankrijk', 'Senegal': 'Senegal', 'Iraq': 'Irak', 'Norway': 'Noorwegen', 'Argentina': 'Argentinië', 'Algeria': 'Algerije', 'Austria': 'Oostenrijk', 'Jordan': 'Jordanië', 'Portugal': 'Portugal', 'DR Congo': 'Congo DR', 'Uzbekistan': 'Oezbekistan', 'Colombia': 'Colombia', 'England': 'Engeland', 'Croatia': 'Kroatië', 'Ghana': 'Ghana', 'Panama': 'Panama', }; let updated = 0; for(const match of data.matches){ if(!match.score) continue; // not played yet const ft = match.score.ft; if(!ft || ft.length < 2) continue; const homeNL = nameMap[match.team1] || match.team1; const awayNL = nameMap[match.team2] || match.team2; const homeScore = ft[0]; const awayScore = ft[1]; // Find matching match in our GROUPS let foundId = null; for(const g of GROUPS){ for(const m of g.matches){ if(m.home === homeNL && m.away === awayNL){ foundId = m.id; break; } } if(foundId) break; } if(!foundId) continue; // Only update if different from current const current = actualResults[foundId]; if(current && current.home === homeScore && current.away === awayScore) continue; actualResults[foundId] = {home: homeScore, away: awayScore}; await sb.from('results').upsert({ match_id: foundId, home_score: homeScore, away_score: awayScore, updated_at: new Date().toISOString(), }, {onConflict: 'match_id'}); updated++; } // ── Sync knockout wedstrijden ── let koUpdated = 0; // Load current ko labels from DB const koLabels = await sb.from('knockout_labels').select('*'); const currentLabels = {}; if(!koLabels.error) (koLabels.data||[]).forEach(r => { currentLabels[r.match_id] = {home: r.home_team, away: r.away_team}; }); for(const match of data.matches){ const homeNL = nameMap[match.team1] || match.team1; const awayNL = nameMap[match.team2] || match.team2; // Find matching KO match by comparing team names against known labels let foundKoId = null; for(const round of KNOCKOUT_ROUNDS){ for(const m of round.matches){ // Match by current label (home/away team already known) or by score teams const lbl = currentLabels[m.id]; if(lbl && lbl.home === homeNL && lbl.away === awayNL){ foundKoId = m.id; break; } } if(foundKoId) break; } // Update label if teams are now known (not in group stage) if(!foundKoId){ // Try to match by round order: openfootball numbers R32 matches 73-88, R16 89-96 etc. const mNum = parseInt(match.num); if(mNum >= 73 && mNum <= 88){ // Zestiende finales: match 73=ko1, 74=ko2 ... but order may vary // Use a positional map based on match number const koOrder = ['ko1','ko2','ko3','ko4','ko5','ko6','ko7','ko8', 'ko9','ko10','ko11','ko12','ko13','ko14','ko15','ko16']; foundKoId = koOrder[mNum - 73] || null; } else if(mNum >= 89 && mNum <= 96){ const afOrder = ['af1','af2','af3','af4','af5','af6','af7','af8']; foundKoId = afOrder[mNum - 89] || null; } else if(mNum >= 97 && mNum <= 100){ const kfOrder = ['kf1','kf2','kf3','kf4']; foundKoId = kfOrder[mNum - 97] || null; } else if(mNum === 101){ foundKoId = 'hf1'; } else if(mNum === 102){ foundKoId = 'hf2'; } else if(mNum === 103){ foundKoId = 'fin3'; } else if(mNum === 104){ foundKoId = 'fin1'; } } if(!foundKoId) continue; // Update team label in DB and local KNOCKOUT_ROUNDS const existingLabel = currentLabels[foundKoId]; if(!existingLabel || existingLabel.home !== homeNL || existingLabel.away !== awayNL){ await sb.from('knockout_labels').upsert({ match_id: foundKoId, home_team: homeNL, away_team: awayNL }, {onConflict: 'match_id'}); currentLabels[foundKoId] = {home: homeNL, away: awayNL}; // Update label in local KNOCKOUT_ROUNDS for(const round of KNOCKOUT_ROUNDS){ const m = round.matches.find(x => x.id === foundKoId); if(m){ m.label = `${homeNL} vs ${awayNL}`; break; } } koUpdated++; } // Also sync score if played if(!match.score) continue; const ft = match.score.ft; if(!ft || ft.length < 2) continue; const current = actualKo[foundKoId]; if(current && current.home === ft[0] && current.away === ft[1]) continue; actualKo[foundKoId] = {home: ft[0], away: ft[1]}; await sb.from('actual_knockout').upsert({ match_id: foundKoId, home_score: ft[0], away_score: ft[1] }, {onConflict: 'match_id'}); koUpdated++; } // Apply saved labels to KNOCKOUT_ROUNDS on load for(const [id, lbl] of Object.entries(currentLabels)){ for(const round of KNOCKOUT_ROUNDS){ const m = round.matches.find(x => x.id === id); if(m){ m.label = `${lbl.home} vs ${lbl.away}`; break; } } } await loadLeaderboard(); if(syncBtn){ syncBtn.textContent = `✓ ${updated} groep + ${koUpdated} knockout bijgewerkt`; syncBtn.disabled = false; } render(); } catch(e) { if(syncBtn){ syncBtn.textContent = '⚠ Fout — probeer opnieuw'; syncBtn.disabled = false; } console.error('Sync error:', e); } } function switchTab(t){ tab = t; if(t==='overzicht') overzichtData = null; if(t==='dashboard') dashboardData = null; if(t==='lb') loadLeaderboard().then(render); else render(); } // ── COUNTDOWN ── function updateCountdown(){ const el = document.getElementById('countdown'); if(!el) return; const now = new Date(); const diff = DEADLINE - now; if(diff <= 0){ el.innerHTML = '⏰ Deadline verstreken'; return; } const days = Math.floor(diff / 86400000); const hours = Math.floor((diff % 86400000) / 3600000); const mins = Math.floor((diff % 3600000) / 60000); const secs = Math.floor((diff % 60000) / 1000); el.innerHTML = ` ${String(days).padStart(2,'0')}dagen : ${String(hours).padStart(2,'0')}uur : ${String(mins).padStart(2,'0')}min : ${String(secs).padStart(2,'0')}sec `; } setInterval(updateCountdown, 1000); // ── INIT ── render(); updateCountdown();