Viel Push und achivements + AGB

This commit is contained in:
2025-09-16 23:41:34 +02:00
parent b2fc63e2d0
commit 5831d1bb91
8 changed files with 934 additions and 49 deletions

239
public/agb.html Normal file
View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Allgemeine Geschäftsbedingungen | NinjaCross</title>
<link rel="stylesheet" href="css/dashboard.css">
<style>
.agb-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #1e293b;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-top: 20px;
color: #e2e8f0;
}
.agb-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #334155;
}
.agb-header h1 {
color: #00d4ff;
margin-bottom: 10px;
}
.agb-header .subtitle {
color: #94a3b8;
font-size: 16px;
}
.agb-content {
line-height: 1.6;
color: #e2e8f0;
}
.agb-section {
margin-bottom: 25px;
}
.agb-section h2 {
color: #00d4ff;
font-size: 20px;
margin-bottom: 15px;
padding-bottom: 5px;
border-bottom: 1px solid #334155;
}
.agb-section h3 {
color: #e2e8f0;
font-size: 16px;
margin-bottom: 10px;
margin-top: 20px;
}
.agb-section p {
margin-bottom: 12px;
}
.agb-section ul {
margin-left: 20px;
margin-bottom: 15px;
}
.agb-section li {
margin-bottom: 8px;
}
.highlight-box {
background: #0f172a;
border-left: 4px solid #00d4ff;
padding: 15px;
margin: 20px 0;
border-radius: 0 5px 5px 0;
}
.warning-box {
background: #451a03;
border-left: 4px solid #fbbf24;
padding: 15px;
margin: 20px 0;
border-radius: 0 5px 5px 0;
}
.back-button {
display: inline-block;
background: #00d4ff;
color: #0f172a;
padding: 12px 24px;
text-decoration: none;
border-radius: 5px;
margin-top: 30px;
transition: background-color 0.3s;
font-weight: bold;
}
.back-button:hover {
background: #0891b2;
}
.last-updated {
text-align: center;
color: #94a3b8;
font-style: italic;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #334155;
}
</style>
</head>
<body style="background: #0f172a; min-height: 100vh; padding: 20px;">
<div class="agb-container">
<div class="agb-header">
<h1>Allgemeine Geschäftsbedingungen</h1>
<p class="subtitle">NinjaCross Parkour System</p>
</div>
<div class="agb-content">
<div class="highlight-box">
<strong>Wichtig:</strong> Durch die Nutzung des NinjaCross Systems stimmen Sie zu, dass Ihre Daten
(Name, Zeiten, Standorte) im öffentlichen Leaderboard angezeigt werden können.
</div>
<div class="agb-section">
<h2>1. Geltungsbereich</h2>
<p>Diese Allgemeinen Geschäftsbedingungen (AGB) gelten für die Nutzung des NinjaCross Parkour Systems
im Schwimmbad. Mit der Registrierung und Nutzung des Systems erkennen Sie diese AGB als verbindlich an.</p>
</div>
<div class="agb-section">
<h2>2. Datenverarbeitung und Datenschutz</h2>
<h3>2.1 Erhebung von Daten</h3>
<p>Wir erheben folgende personenbezogene Daten:</p>
<ul>
<li>Vor- und Nachname</li>
<li>Geburtsdatum (zur Altersberechnung)</li>
<li>RFID-Kartennummer</li>
<li>Laufzeiten und Standortdaten</li>
<li>Zeitstempel der Aktivitäten</li>
</ul>
<h3>2.2 Verwendung der Daten</h3>
<p>Ihre Daten werden für folgende Zwecke verwendet:</p>
<ul>
<li><strong>Leaderboard-Anzeige:</strong> Name, Zeiten und Standorte werden im öffentlichen Leaderboard angezeigt</li>
<li><strong>Leistungsauswertung:</strong> Erfassung und Bewertung Ihrer Parkour-Zeiten</li>
<li><strong>System-Funktionalität:</strong> Zuordnung von Zeiten zu Ihrem Profil über RFID-Karten</li>
<li><strong>Statistiken:</strong> Anonymisierte Auswertungen für Systemverbesserungen</li>
</ul>
<div class="warning-box">
<strong>Wichtiger Hinweis:</strong> Durch die Annahme dieser AGB stimmen Sie ausdrücklich zu,
dass Ihr Name und Ihre Laufzeiten im öffentlichen Leaderboard sichtbar sind.
</div>
</div>
<div class="agb-section">
<h2>3. Leaderboard und Öffentlichkeit</h2>
<p>Das NinjaCross System verfügt über ein öffentlich zugängliches Leaderboard, das folgende Informationen anzeigt:</p>
<ul>
<li>Vollständiger Name der Teilnehmer</li>
<li>Erreichte Laufzeiten</li>
<li>Standort der Aktivität</li>
<li>Datum und Uhrzeit der Aktivität</li>
</ul>
<p><strong>Durch die Nutzung des Systems erklären Sie sich damit einverstanden, dass diese Daten öffentlich angezeigt werden.</strong></p>
</div>
<div class="agb-section">
<h2>4. Ihre Rechte</h2>
<h3>4.1 Recht auf Auskunft</h3>
<p>Sie haben das Recht, Auskunft über die zu Ihrer Person gespeicherten Daten zu verlangen.</p>
<h3>4.2 Recht auf Löschung</h3>
<p>Sie können jederzeit die Löschung Ihrer Daten und Ihres Profils beantragen.</p>
<h3>4.3 Recht auf Widerspruch</h3>
<p>Sie können der Verarbeitung Ihrer Daten für das Leaderboard widersprechen.
In diesem Fall werden Ihre Daten aus dem öffentlichen Leaderboard entfernt,
aber weiterhin für die Systemfunktionalität verwendet.</p>
</div>
<div class="agb-section">
<h2>5. Haftung und Verantwortung</h2>
<p>Die Teilnahme am NinjaCross System erfolgt auf eigene Gefahr. Wir haften nicht für:</p>
<ul>
<li>Verletzungen während der Nutzung der Parkour-Anlage</li>
<li>Verlust oder Diebstahl der RFID-Karte</li>
<li>Technische Ausfälle des Systems</li>
</ul>
</div>
<div class="agb-section">
<h2>6. Systemregeln</h2>
<p>Bei der Nutzung des Systems sind folgende Regeln zu beachten:</p>
<ul>
<li>Keine Manipulation der Zeiterfassung</li>
<li>Respektvoller Umgang mit anderen Teilnehmern</li>
<li>Beachtung der Sicherheitshinweise der Anlage</li>
<li>Keine Verwendung falscher Identitäten</li>
</ul>
</div>
<div class="agb-section">
<h2>7. Änderungen der AGB</h2>
<p>Wir behalten uns vor, diese AGB zu ändern. Wesentliche Änderungen werden Ihnen
mitgeteilt und erfordern Ihre erneute Zustimmung.</p>
</div>
<div class="agb-section">
<h2>8. Kontakt</h2>
<p>Bei Fragen zu diesen AGB oder zum Datenschutz wenden Sie sich an:</p>
<p>
<strong>NinjaCross Team</strong><br>
Schwimmbad Ulm<br>
E-Mail: info@ninjacross.de<br>
Telefon: 0731-123456
</p>
</div>
</div>
<div style="text-align: center;">
<a href="javascript:history.back()" class="back-button">Zurück</a>
</div>
<div class="last-updated">
<p>Stand: September 2024</p>
<p>Diese AGB sind Teil der Registrierung und gelten ab dem Zeitpunkt der Zustimmung.</p>
</div>
</div>
</body>
</html>

View File

@@ -703,6 +703,21 @@
<input type="date" id="playerBirthdate" class="form-input" style="text-align: center;">
</div>
<!-- AGB Section -->
<div class="agb-section" style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 15px; margin: 15px 0;">
<div class="agb-checkbox" style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px;">
<input type="checkbox" id="agbAccepted" name="agbAccepted" required style="width: auto; margin: 0; margin-top: 3px;">
<label for="agbAccepted" style="color: #e2e8f0; font-size: 0.85rem; line-height: 1.4; margin: 0; font-weight: normal;">
Ich habe die <a href="/agb.html" target="_blank" style="color: #00d4ff; text-decoration: none; font-weight: bold;">Allgemeinen Geschäftsbedingungen</a>
gelesen und stimme zu, dass mein Name und meine Laufzeiten im öffentlichen Leaderboard angezeigt werden.
</label>
</div>
<div class="agb-warning" style="color: #fbbf24; font-size: 0.8rem; margin-top: 10px;">
⚠️ <strong>Wichtig:</strong> Ohne Zustimmung zu den AGB können Sie das System nutzen,
aber Ihre Zeiten werden nicht im öffentlichen Leaderboard angezeigt.
</div>
</div>
<button class="btn btn-primary" onclick="createRfidPlayerRecord()" style="width: 100%;" data-de="Spieler erstellen" data-en="Create Player">
Spieler erstellen
</button>
@@ -768,6 +783,6 @@
</footer>
<script src="/js/cookie-consent.js"></script>
<script src="/js/dashboard.js?v=1.1"></script>
<script src="/js/dashboard.js?v=1.6"></script>
</body>
</html>

View File

@@ -400,7 +400,7 @@ function filterData() {
displayAchievements();
} else {
currentPlayers = filteredData;
displayPlayers();
displayPlayersWithAchievements();
}
break;
}
@@ -427,7 +427,7 @@ function refreshData() {
if (currentAchievementMode === 'achievements') {
loadAchievements();
} else {
loadPlayers();
loadPlayersWithAchievements();
}
break;
}
@@ -1372,7 +1372,7 @@ async function toggleAchievementMode() {
currentAchievementMode = 'players';
document.getElementById('dataTitle').textContent = '👥 Spieler-Achievements';
document.getElementById('searchInput').placeholder = 'Spieler durchsuchen...';
await loadPlayers();
await loadPlayersWithAchievements();
} else {
currentAchievementMode = 'achievements';
document.getElementById('dataTitle').textContent = '🏆 Achievement-Verwaltung';
@@ -1382,7 +1382,7 @@ async function toggleAchievementMode() {
}
// Load all players with achievement statistics
async function loadPlayers() {
async function loadPlayersWithAchievements() {
try {
const response = await fetch('/api/v1/admin/achievements/players');
const result = await response.json();
@@ -1390,7 +1390,7 @@ async function loadPlayers() {
if (result.success) {
currentPlayers = result.data;
currentData = result.data; // Set for filtering
displayPlayers();
displayPlayersWithAchievements();
} else {
showError('Fehler beim Laden der Spieler: ' + result.message);
}
@@ -1401,7 +1401,7 @@ async function loadPlayers() {
}
// Display players in table
function displayPlayers() {
function displayPlayersWithAchievements() {
const content = document.getElementById('dataContent');
if (currentPlayers.length === 0) {

View File

@@ -618,6 +618,7 @@ async function createRfidPlayerRecord() {
const firstname = document.getElementById('playerFirstname').value.trim();
const lastname = document.getElementById('playerLastname').value.trim();
const birthdate = document.getElementById('playerBirthdate').value;
const agbAccepted = document.getElementById('agbAccepted').checked;
// Validation
if (!rawUid) {
@@ -652,6 +653,14 @@ async function createRfidPlayerRecord() {
return;
}
if (!agbAccepted) {
const agbErrorMsg = currentLanguage === 'de' ?
'Bitte stimme den Allgemeinen Geschäftsbedingungen zu' :
'Please accept the Terms of Service';
showMessage('rfidMessage', agbErrorMsg, 'error');
return;
}
try {
// Format the UID to match database format
const formattedUid = formatRfidUid(rawUid);
@@ -672,7 +681,8 @@ async function createRfidPlayerRecord() {
firstname: firstname,
lastname: lastname,
birthdate: birthdate,
supabase_user_id: currentUser?.id || null
supabase_user_id: currentUser?.id || null,
agb_accepted: agbAccepted
})
});
@@ -689,6 +699,7 @@ async function createRfidPlayerRecord() {
document.getElementById('playerFirstname').value = '';
document.getElementById('playerLastname').value = '';
document.getElementById('playerBirthdate').value = '';
document.getElementById('agbAccepted').checked = false;
// Hide create player section since user is now linked
const createPlayerSection = document.getElementById('createPlayerSection');
@@ -1139,7 +1150,7 @@ let currentAchievementCategory = 'all';
// Load achievements for the current player
async function loadPlayerAchievements() {
if (!currentPlayerId) {
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
showAchievementsNotAvailable();
return;
}
@@ -1372,7 +1383,7 @@ function showStatistics() {
async function loadAnalyticsData() {
try {
if (!currentPlayerId) {
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
console.error('No player ID available - user not linked');
// Show fallback data when user is not linked
displayAnalyticsFallback();
@@ -1400,7 +1411,7 @@ async function loadAnalyticsData() {
async function loadStatisticsData() {
try {
if (!currentPlayerId) {
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
console.error('No player ID available - user not linked');
// Show fallback data when user is not linked
displayStatisticsFallback();
@@ -1682,7 +1693,7 @@ function showAchievementsNotAvailable() {
// Check achievements for current player
async function checkPlayerAchievements() {
if (!currentPlayerId) return;
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') return;
try {
const response = await fetch(`/api/achievements/check/${currentPlayerId}?t=${Date.now()}`, {
@@ -1738,8 +1749,13 @@ function showAchievementNotification(newAchievements) {
// Initialize achievements when player is loaded
function initializeAchievements(playerId) {
currentPlayerId = playerId;
loadPlayerAchievements();
if (playerId && playerId !== 'undefined' && playerId !== 'null') {
currentPlayerId = playerId;
loadPlayerAchievements();
} else {
console.warn('Invalid player ID provided to initializeAchievements:', playerId);
currentPlayerId = null;
}
}
// Convert VAPID key from base64url to Uint8Array
@@ -1764,6 +1780,16 @@ function urlBase64ToUint8Array(base64String) {
// Web Notification Functions
function showWebNotification(title, message, icon = '🏆') {
if ('Notification' in window && Notification.permission === 'granted') {
// Log notification details to console
console.log('🔔 Web Notification sent:', {
title: title,
message: message,
icon: icon,
playerId: currentPlayerId || 'unknown',
pushPlayerId: localStorage.getItem('pushPlayerId') || 'unknown',
timestamp: new Date().toISOString()
});
const notification = new Notification(title, {
body: message,
icon: '/pictures/icon-192.png',
@@ -1809,7 +1835,7 @@ async function checkBestTimeNotifications() {
const { daily, weekly, monthly } = result.data;
// Check if current player has best times
if (currentPlayerId) {
if (currentPlayerId && currentPlayerId !== 'undefined' && currentPlayerId !== 'null') {
const now = new Date();
const isEvening = now.getHours() >= 19;
@@ -1910,30 +1936,87 @@ async function checkBestTimeNotifications() {
// Check for new achievements and show notifications
async function checkAchievementNotifications() {
try {
if (!currentPlayerId) return;
console.log('🔍 checkAchievementNotifications() called');
// Check if push notifications are enabled
const pushPlayerId = localStorage.getItem('pushPlayerId');
if (!pushPlayerId) {
console.log('🔕 Push notifications disabled, skipping achievement check');
return;
}
console.log('🔍 Push notifications enabled for player:', pushPlayerId);
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
// Use pushPlayerId for notifications instead of currentPlayerId
if (!pushPlayerId || pushPlayerId === 'undefined' || pushPlayerId === 'null') return;
const response = await fetch(`/api/achievements/player/${pushPlayerId}?t=${Date.now()}`);
const result = await response.json();
if (result.success && result.data) {
const newAchievements = result.data.filter(achievement => {
// Check if achievement was earned in the last 5 minutes
const earnedAt = new Date(achievement.earned_at);
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
return earnedAt > fiveMinutesAgo;
console.log('🔍 Checking achievements for notifications:', {
totalAchievements: result.data.length,
playerId: pushPlayerId
});
const newAchievements = result.data.filter(achievement => {
// Only check completed achievements
if (!achievement.is_completed) {
return false;
}
// Check if achievement was earned in the last 10 minutes (extended window)
const earnedAt = achievement.earned_at ? new Date(achievement.earned_at) : null;
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
// If no earned_at date, check if it was completed recently by checking completion_count
if (!earnedAt) {
// Send to server console
fetch('/api/v1/public/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: 'info',
message: `🔍 Achievement completed but no earned_at: ${achievement.name} (completion_count: ${achievement.completion_count})`,
playerId: pushPlayerId
})
}).catch(() => {}); // Ignore errors
// For achievements without earned_at, assume they are new if completion_count is 1
// This is a fallback for recently completed achievements
return achievement.completion_count === 1;
}
const isNew = earnedAt > tenMinutesAgo;
// Send detailed info to server console
fetch('/api/v1/public/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: 'info',
message: `🔍 Achievement check: ${achievement.name} - earned_at: ${earnedAt.toISOString()}, isNew: ${isNew}, completed: ${achievement.is_completed}`,
playerId: pushPlayerId
})
}).catch(() => {}); // Ignore errors
return isNew;
});
console.log('🔍 New achievements found:', newAchievements.length);
if (newAchievements.length > 0) {
for (const achievement of newAchievements) {
// Check if notification was already sent for this achievement
const achievementCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/achievement?achievementId=${achievement.achievement_id}&locationId=${achievement.location_id || ''}`);
const achievementId = achievement.id || achievement.achievement_id;
const locationId = achievement.location_id || '';
if (!achievementId) {
console.warn('Achievement ID is missing for achievement:', achievement);
continue;
}
const achievementCheck = await fetch(`/api/v1/public/notification-sent/${pushPlayerId}/achievement?achievementId=${achievementId}&locationId=${locationId}`);
const achievementResult = await achievementCheck.json();
if (!achievementResult.wasSent) {
@@ -1949,10 +2032,10 @@ async function checkAchievementNotifications() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
playerId: pushPlayerId,
notificationType: 'achievement',
achievementId: achievement.achievement_id,
locationId: achievement.location_id || null
achievementId: achievementId,
locationId: locationId || null
})
});
console.log(`🏆 Achievement notification sent: ${achievement.name}`);
@@ -2015,7 +2098,7 @@ function updateLeaderboardSetting() {
async function saveSettings() {
try {
if (!currentPlayerId) {
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
console.error('No player ID available');
return;
}