Zulassen von Anonymen RFID updates verlinkung der UUID wenn spieler angelegt wurde
This commit is contained in:
@@ -99,6 +99,20 @@
|
||||
<button class="btn" onclick="showPlayerManagement()">Spieler anzeigen</button>
|
||||
</div>
|
||||
|
||||
<!-- Blacklist-Verwaltung -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">🚫</span> Blacklist-Verwaltung</h3>
|
||||
<p>Verwalte verbotene Namen und Begriffe</p>
|
||||
<button class="btn" onclick="showBlacklistManagement()">Blacklist verwalten</button>
|
||||
</div>
|
||||
|
||||
<!-- KI-Moderator -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">🧠</span> KI-Moderator</h3>
|
||||
<p>Intelligente Namensprüfung mit Ollama LLM</p>
|
||||
<button class="btn" onclick="showLLMManagement()">KI-Moderator testen</button>
|
||||
</div>
|
||||
|
||||
<!-- Läufe-Verwaltung -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">⏱️</span> Läufe-Verwaltung</h3>
|
||||
@@ -168,6 +182,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blacklist Management Modal -->
|
||||
<div id="blacklistModal" class="modal">
|
||||
<div class="modal-content" style="max-width: 800px;">
|
||||
<span class="close" onclick="closeModal('blacklistModal')">×</span>
|
||||
<h3>🚫 Blacklist-Verwaltung</h3>
|
||||
<div class="message" id="blacklistMessage"></div>
|
||||
|
||||
<!-- Test Name Section -->
|
||||
<div style="border: 1px solid #ddd; padding: 1rem; margin-bottom: 1rem; border-radius: 5px;">
|
||||
<h4>Name testen</h4>
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||
<input type="text" id="testFirstname" placeholder="Vorname" style="flex: 1; padding: 0.5rem;">
|
||||
<input type="text" id="testLastname" placeholder="Nachname" style="flex: 1; padding: 0.5rem;">
|
||||
<button class="btn" onclick="testNameAgainstBlacklist()">Testen</button>
|
||||
</div>
|
||||
<div id="testResult" style="padding: 0.5rem; border-radius: 3px; display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Add New Entry Section -->
|
||||
<div style="border: 1px solid #ddd; padding: 1rem; margin-bottom: 1rem; border-radius: 5px;">
|
||||
<h4>Neuen Eintrag hinzufügen</h4>
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||
<input type="text" id="newTerm" placeholder="Begriff" style="flex: 1; padding: 0.5rem;">
|
||||
<select id="newCategory" style="flex: 1; padding: 0.5rem;">
|
||||
<option value="historical">Historisch belastet</option>
|
||||
<option value="offensive">Beleidigend/anstößig</option>
|
||||
<option value="titles">Titel/Berufsbezeichnung</option>
|
||||
<option value="brands">Markenname</option>
|
||||
<option value="inappropriate">Unpassend</option>
|
||||
</select>
|
||||
<button class="btn btn-success" onclick="addToBlacklist()">Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blacklist Content -->
|
||||
<div id="blacklistContent">
|
||||
<div class="loading">Lade Blacklist...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
@@ -182,6 +237,50 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- LLM-Moderator Modal -->
|
||||
<div id="llmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeLLMModal()">×</span>
|
||||
<h2>🧠 KI-Moderator</h2>
|
||||
|
||||
<!-- LLM-Status -->
|
||||
<div id="llmStatus" class="llm-status-section">
|
||||
<h3>Status</h3>
|
||||
<div id="llmStatusContent">Lade...</div>
|
||||
</div>
|
||||
|
||||
<!-- Name testen -->
|
||||
<div class="llm-test-section">
|
||||
<h3>Name testen</h3>
|
||||
<div class="form-group">
|
||||
<label for="llmFirstname">Vorname:</label>
|
||||
<input type="text" id="llmFirstname" placeholder="Vorname eingeben">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="llmLastname">Nachname:</label>
|
||||
<input type="text" id="llmLastname" placeholder="Nachname eingeben">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="llmContext">Kontext (optional):</label>
|
||||
<input type="text" id="llmContext" placeholder="Zusätzlicher Kontext">
|
||||
</div>
|
||||
<button onclick="testNameWithLLM()" class="btn btn-primary">Mit KI prüfen</button>
|
||||
</div>
|
||||
|
||||
<!-- Ergebnis -->
|
||||
<div id="llmResult" class="llm-result-section" style="display: none;">
|
||||
<h3>Ergebnis</h3>
|
||||
<div id="llmResultContent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Vergleich mit Blacklist -->
|
||||
<div id="llmComparison" class="llm-comparison-section" style="display: none;">
|
||||
<h3>Vergleich mit Blacklist</h3>
|
||||
<div id="llmComparisonContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application JavaScript -->
|
||||
<script src="/js/cookie-consent.js"></script>
|
||||
<script src="/js/admin-dashboard.js"></script>
|
||||
|
||||
@@ -204,11 +204,13 @@ body {
|
||||
|
||||
.modal-content {
|
||||
background-color: rgba(26, 26, 46, 0.95);
|
||||
margin: 10% auto;
|
||||
margin: 5% auto;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -228,6 +230,37 @@ body {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Blacklist Modal specific styles */
|
||||
#blacklistModal .modal-content {
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Smooth scrolling for modal content */
|
||||
.modal-content {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for modal content */
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 116, 139, 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.8);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@@ -239,6 +239,8 @@ body {
|
||||
border-radius: 1rem;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
@@ -260,7 +262,36 @@ body {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
/* RFID Modal specific styles */
|
||||
#rfidModal .modal-content {
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Smooth scrolling for modal content */
|
||||
.modal-content {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for modal content */
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 116, 139, 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.8);
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
|
||||
@@ -85,7 +85,7 @@ body {
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
@@ -204,7 +204,6 @@ body {
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.25rem;
|
||||
gap: 0.25rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.time-tab {
|
||||
@@ -255,7 +254,7 @@ body {
|
||||
|
||||
.refresh-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #00d4ff, #0891b2);
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
@@ -266,6 +265,7 @@ body {
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
@@ -281,6 +281,65 @@ body {
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.admin-panel {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.admin-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.admin-btn {
|
||||
padding: 1rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-login-btn {
|
||||
background: linear-gradient(135deg, #ff6b35, #f7931e);
|
||||
}
|
||||
|
||||
.admin-login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.dashboard-btn {
|
||||
background: linear-gradient(135deg, #00d4ff, #0891b2);
|
||||
}
|
||||
|
||||
.dashboard-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: linear-gradient(135deg, #dc3545, #c82333);
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@@ -459,75 +518,6 @@ body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Admin Login Button */
|
||||
.admin-login-btn {
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #ff6b35, #f7931e);
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.admin-login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.dashboard-btn {
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #00d4ff, #0891b2);
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dashboard-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
right: 12rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #dc3545, #c82333);
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.pulse-animation {
|
||||
animation: pulse 2s infinite;
|
||||
@@ -650,13 +640,17 @@ body {
|
||||
/* Mobile First Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.admin-panel {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -676,8 +670,7 @@ body {
|
||||
|
||||
.header-section {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 5rem;
|
||||
/* Account for fixed buttons */
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
@@ -685,6 +678,22 @@ body {
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.admin-panel {
|
||||
grid-column: 1;
|
||||
order: -1; /* Admin panel kommt zuerst */
|
||||
}
|
||||
|
||||
.admin-buttons {
|
||||
flex-direction: row;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
padding: 1.5rem;
|
||||
@@ -817,44 +826,6 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-login-btn,
|
||||
.dashboard-btn,
|
||||
.logout-btn {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
right: 8rem;
|
||||
}
|
||||
|
||||
/* Mobile button container for better positioning */
|
||||
.mobile-nav-buttons {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.mobile-nav-buttons .admin-login-btn,
|
||||
.mobile-nav-buttons .dashboard-btn,
|
||||
.mobile-nav-buttons .logout-btn {
|
||||
position: static;
|
||||
right: auto;
|
||||
width: 120px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notification-bubble {
|
||||
top: 1rem;
|
||||
@@ -895,6 +866,20 @@ body {
|
||||
.stats-panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.admin-panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.admin-buttons {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.admin-btn {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.leaderboard-header {
|
||||
padding: 1rem;
|
||||
@@ -947,18 +932,6 @@ body {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.mobile-nav-buttons {
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-nav-buttons .admin-login-btn,
|
||||
.mobile-nav-buttons .dashboard-btn,
|
||||
.mobile-nav-buttons .logout-btn {
|
||||
width: 100px;
|
||||
padding: 0.4rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Landscape orientation on mobile */
|
||||
@@ -972,19 +945,6 @@ body {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.mobile-nav-buttons {
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-nav-buttons .admin-login-btn,
|
||||
.mobile-nav-buttons .dashboard-btn,
|
||||
.mobile-nav-buttons .logout-btn {
|
||||
width: auto;
|
||||
min-width: 80px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.time-tabs {
|
||||
flex-direction: row;
|
||||
|
||||
@@ -296,6 +296,32 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Create New Player Section -->
|
||||
<div id="createPlayerSection" style="border-top: 1px solid #334155; padding-top: 1.5rem; margin-top: 1.5rem;">
|
||||
<p style="color: #8892b0; text-align: center; margin-bottom: 1rem; font-size: 0.9rem;" data-de="Neuen Spieler mit RFID erstellen:" data-en="Create new player with RFID:">
|
||||
Neuen Spieler mit RFID erstellen:
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playerFirstname" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Vorname:" data-en="First Name:">Vorname:</label>
|
||||
<input type="text" id="playerFirstname" class="form-input" placeholder="Max" style="text-align: center;">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playerLastname" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Nachname:" data-en="Last Name:">Nachname:</label>
|
||||
<input type="text" id="playerLastname" class="form-input" placeholder="Mustermann" style="text-align: center;">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playerBirthdate" style="color: #8892b0; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;" data-de="Geburtsdatum:" data-en="Birth Date:">Geburtsdatum:</label>
|
||||
<input type="date" id="playerBirthdate" class="form-input" style="text-align: center;">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="createRfidPlayerRecord()" style="width: 100%;" data-de="Spieler erstellen" data-en="Create Player">
|
||||
Spieler erstellen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Scanning Status -->
|
||||
<div id="scanningStatus" style="display: none; text-align: center; color: #00d4ff; margin-top: 1rem;">
|
||||
<div class="spinner" style="width: 20px; height: 20px; margin: 0 auto 0.5rem;"></div>
|
||||
|
||||
@@ -3,14 +3,14 @@ let currentDataType = null;
|
||||
let currentData = [];
|
||||
|
||||
// Beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
checkAuth();
|
||||
loadStatistics();
|
||||
|
||||
|
||||
// Add cookie settings button functionality
|
||||
const cookieSettingsBtn = document.getElementById('cookie-settings-footer');
|
||||
if (cookieSettingsBtn) {
|
||||
cookieSettingsBtn.addEventListener('click', function() {
|
||||
cookieSettingsBtn.addEventListener('click', function () {
|
||||
if (window.cookieConsent) {
|
||||
window.cookieConsent.resetConsent();
|
||||
}
|
||||
@@ -23,9 +23,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function setupEventListeners() {
|
||||
// Logout Button
|
||||
document.getElementById('logoutBtn').addEventListener('click', logout);
|
||||
|
||||
|
||||
// Generator Button
|
||||
document.getElementById('generatorBtn').addEventListener('click', function() {
|
||||
document.getElementById('generatorBtn').addEventListener('click', function () {
|
||||
window.location.href = '/generator';
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/web/check-session');
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (!result.success) {
|
||||
window.location.href = '/adminlogin.html';
|
||||
return;
|
||||
@@ -56,7 +56,7 @@ async function checkAuth() {
|
||||
|
||||
currentUser = result.user;
|
||||
document.getElementById('username').textContent = currentUser.username;
|
||||
|
||||
|
||||
const accessBadge = document.getElementById('accessBadge');
|
||||
accessBadge.textContent = `Level ${currentUser.access_level}`;
|
||||
accessBadge.className = `access-badge level-${currentUser.access_level}`;
|
||||
@@ -86,7 +86,7 @@ async function loadStatistics() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/stats');
|
||||
const stats = await response.json();
|
||||
|
||||
|
||||
if (stats.success) {
|
||||
document.getElementById('totalPlayers').textContent = stats.data.players || 0;
|
||||
document.getElementById('totalRuns').textContent = stats.data.runs || 0;
|
||||
@@ -102,16 +102,16 @@ async function loadPageStatistics() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/page-stats');
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
|
||||
|
||||
// Display page view statistics
|
||||
displayPageStats('todayStats', data.today);
|
||||
displayPageStats('weekStats', data.week);
|
||||
displayPageStats('monthStats', data.month);
|
||||
displayPageStats('totalStats', data.total);
|
||||
|
||||
|
||||
// Display link statistics
|
||||
if (data.linkStats) {
|
||||
document.getElementById('totalPlayersCount').textContent = data.linkStats.total_players || 0;
|
||||
@@ -130,16 +130,16 @@ async function loadPageStatistics() {
|
||||
|
||||
function displayPageStats(containerId, stats) {
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
|
||||
if (!stats || stats.length === 0) {
|
||||
container.innerHTML = '<div style="color: #6c757d;">0</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Find main page visits
|
||||
const mainPageStat = stats.find(stat => stat.page === 'main_page_visit');
|
||||
const count = mainPageStat ? mainPageStat.count : 0;
|
||||
|
||||
|
||||
container.innerHTML = `<div style="font-size: 1.5em; font-weight: bold; color: #00d4ff;">${count}</div>`;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ function getPageDisplayName(page) {
|
||||
'license_generator': '🔧 Lizenzgenerator',
|
||||
'reset_password': '🔑 Passwort Reset'
|
||||
};
|
||||
|
||||
|
||||
return pageNames[page] || page;
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ async function loadPlayers() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/players');
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
currentData = result.data;
|
||||
displayPlayersTable(result.data);
|
||||
@@ -221,7 +221,7 @@ async function loadRuns() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/runs');
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
currentData = result.data;
|
||||
displayRunsTable(result.data);
|
||||
@@ -238,7 +238,7 @@ async function loadLocations() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/locations');
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
currentData = result.data;
|
||||
displayLocationsTable(result.data);
|
||||
@@ -255,7 +255,7 @@ async function loadAdminUsers() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/adminusers');
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
currentData = result.data;
|
||||
displayAdminUsersTable(result.data);
|
||||
@@ -271,7 +271,7 @@ async function loadAdminUsers() {
|
||||
function displayPlayersTable(players) {
|
||||
let html = '<div class="table-container"><table class="data-table">';
|
||||
html += '<thead><tr><th>ID</th><th>Name</th><th>RFID UID</th><th>Supabase User</th><th>Registriert</th><th>Aktionen</th></tr></thead><tbody>';
|
||||
|
||||
|
||||
players.forEach(player => {
|
||||
html += `<tr>
|
||||
<td>${player.id}</td>
|
||||
@@ -285,7 +285,7 @@ function displayPlayersTable(players) {
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('dataContent').innerHTML = html;
|
||||
}
|
||||
@@ -293,7 +293,7 @@ function displayPlayersTable(players) {
|
||||
function displayRunsTable(runs) {
|
||||
let html = '<div class="table-container"><table class="data-table">';
|
||||
html += '<thead><tr><th>ID</th><th>Spieler</th><th>Standort</th><th>Zeit</th><th>Datum</th><th>Aktionen</th></tr></thead><tbody>';
|
||||
|
||||
|
||||
runs.forEach(run => {
|
||||
// Use the time_seconds value from the backend
|
||||
const timeInSeconds = parseFloat(run.time_seconds) || 0;
|
||||
@@ -310,7 +310,7 @@ function displayRunsTable(runs) {
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('dataContent').innerHTML = html;
|
||||
}
|
||||
@@ -318,7 +318,7 @@ function displayRunsTable(runs) {
|
||||
function displayLocationsTable(locations) {
|
||||
let html = '<div class="table-container"><table class="data-table">';
|
||||
html += '<thead><tr><th>ID</th><th>Name</th><th>Latitude</th><th>Longitude</th><th>Erstellt</th><th>Aktionen</th></tr></thead><tbody>';
|
||||
|
||||
|
||||
locations.forEach(location => {
|
||||
html += `<tr>
|
||||
<td>${location.id}</td>
|
||||
@@ -332,7 +332,7 @@ function displayLocationsTable(locations) {
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('dataContent').innerHTML = html;
|
||||
}
|
||||
@@ -340,7 +340,7 @@ function displayLocationsTable(locations) {
|
||||
function displayAdminUsersTable(users) {
|
||||
let html = '<div class="table-container"><table class="data-table">';
|
||||
html += '<thead><tr><th>ID</th><th>Benutzername</th><th>Access Level</th><th>Aktiv</th><th>Letzter Login</th><th>Aktionen</th></tr></thead><tbody>';
|
||||
|
||||
|
||||
users.forEach(user => {
|
||||
const isCurrentUser = user.id === currentUser.id;
|
||||
html += `<tr>
|
||||
@@ -354,7 +354,7 @@ function displayAdminUsersTable(users) {
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('dataContent').innerHTML = html;
|
||||
}
|
||||
@@ -375,12 +375,12 @@ function filterData() {
|
||||
if (!currentData) return;
|
||||
|
||||
let filteredData = currentData.filter(item => {
|
||||
return Object.values(item).some(value =>
|
||||
return Object.values(item).some(value =>
|
||||
value && value.toString().toLowerCase().includes(searchTerm)
|
||||
);
|
||||
});
|
||||
|
||||
switch(currentDataType) {
|
||||
switch (currentDataType) {
|
||||
case 'players':
|
||||
displayPlayersTable(filteredData);
|
||||
break;
|
||||
@@ -397,7 +397,7 @@ function filterData() {
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
switch(currentDataType) {
|
||||
switch (currentDataType) {
|
||||
case 'players':
|
||||
loadPlayers();
|
||||
break;
|
||||
@@ -420,9 +420,9 @@ function showAddModal() {
|
||||
const modal = document.getElementById('addModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const formFields = document.getElementById('formFields');
|
||||
|
||||
|
||||
// Modal-Titel basierend auf aktuellem Datentyp setzen
|
||||
switch(currentDataType) {
|
||||
switch (currentDataType) {
|
||||
case 'players':
|
||||
modalTitle.textContent = 'Neuen Spieler hinzufügen';
|
||||
formFields.innerHTML = `
|
||||
@@ -440,7 +440,7 @@ function showAddModal() {
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
|
||||
case 'locations':
|
||||
modalTitle.textContent = 'Neuen Standort hinzufügen';
|
||||
formFields.innerHTML = `
|
||||
@@ -462,7 +462,7 @@ function showAddModal() {
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
|
||||
case 'adminusers':
|
||||
modalTitle.textContent = 'Neuen Admin-Benutzer hinzufügen';
|
||||
formFields.innerHTML = `
|
||||
@@ -483,7 +483,7 @@ function showAddModal() {
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
|
||||
case 'runs':
|
||||
modalTitle.textContent = 'Neuen Lauf hinzufügen';
|
||||
formFields.innerHTML = `
|
||||
@@ -501,12 +501,12 @@ function showAddModal() {
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
modalTitle.textContent = 'Element hinzufügen';
|
||||
formFields.innerHTML = '<p>Keine Felder für diesen Datentyp verfügbar.</p>';
|
||||
}
|
||||
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -521,21 +521,21 @@ function closeModal() {
|
||||
|
||||
async function handleAddSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
|
||||
// Prüfen ob es sich um eine Edit-Operation handelt
|
||||
const isEdit = data.edit_id;
|
||||
const editId = data.edit_id;
|
||||
delete data.edit_id; // edit_id aus den Daten entfernen
|
||||
|
||||
|
||||
try {
|
||||
let endpoint = '';
|
||||
let successMessage = '';
|
||||
let method = 'POST';
|
||||
|
||||
switch(currentDataType) {
|
||||
|
||||
switch (currentDataType) {
|
||||
case 'players':
|
||||
endpoint = isEdit ? `/api/v1/admin/players/${editId}` : '/api/v1/admin/players';
|
||||
successMessage = isEdit ? 'Spieler erfolgreich aktualisiert' : 'Spieler erfolgreich hinzugefügt';
|
||||
@@ -560,7 +560,7 @@ async function handleAddSubmit(e) {
|
||||
showError('Unbekannter Datentyp');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: method,
|
||||
headers: {
|
||||
@@ -568,9 +568,9 @@ async function handleAddSubmit(e) {
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
showSuccess(successMessage);
|
||||
closeModal();
|
||||
@@ -578,7 +578,7 @@ async function handleAddSubmit(e) {
|
||||
} else {
|
||||
showError(result.message || `Fehler beim ${isEdit ? 'Aktualisieren' : 'Hinzufügen'}`);
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Submit failed:', error);
|
||||
showError(`Fehler beim ${isEdit ? 'Aktualisieren' : 'Hinzufügen'}`);
|
||||
@@ -589,11 +589,11 @@ async function handleAddSubmit(e) {
|
||||
async function editPlayer(id) {
|
||||
const player = currentData.find(p => p.id == id);
|
||||
if (!player) return;
|
||||
|
||||
|
||||
const modal = document.getElementById('addModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const formFields = document.getElementById('formFields');
|
||||
|
||||
|
||||
modalTitle.textContent = 'Spieler bearbeiten';
|
||||
formFields.innerHTML = `
|
||||
<div class="form-group">
|
||||
@@ -610,18 +610,18 @@ async function editPlayer(id) {
|
||||
</div>
|
||||
<input type="hidden" name="edit_id" value="${player.id}">
|
||||
`;
|
||||
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
async function editLocation(id) {
|
||||
const location = currentData.find(l => l.id == id);
|
||||
if (!location) return;
|
||||
|
||||
|
||||
const modal = document.getElementById('addModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const formFields = document.getElementById('formFields');
|
||||
|
||||
|
||||
modalTitle.textContent = 'Standort bearbeiten';
|
||||
formFields.innerHTML = `
|
||||
<div class="form-group">
|
||||
@@ -642,7 +642,7 @@ async function editLocation(id) {
|
||||
</div>
|
||||
<input type="hidden" name="edit_id" value="${location.id}">
|
||||
`;
|
||||
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -651,24 +651,24 @@ async function editRun(id) {
|
||||
// Lade die spezifischen Lauf-Daten direkt von der API
|
||||
const response = await fetch(`/api/admin-runs/${id}`);
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (!result.success) {
|
||||
showError('Fehler beim Laden der Lauf-Daten: ' + (result.message || 'Unbekannter Fehler'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const run = result.data;
|
||||
if (!run) {
|
||||
showError('Lauf nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log('Editing run:', run); // Debug log
|
||||
|
||||
|
||||
const modal = document.getElementById('addModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const formFields = document.getElementById('formFields');
|
||||
|
||||
|
||||
modalTitle.textContent = 'Lauf bearbeiten';
|
||||
formFields.innerHTML = `
|
||||
<div class="form-group">
|
||||
@@ -685,9 +685,9 @@ async function editRun(id) {
|
||||
</div>
|
||||
<input type="hidden" name="edit_id" value="${run.id}">
|
||||
`;
|
||||
|
||||
|
||||
modal.style.display = 'block';
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading run data:', error);
|
||||
showError('Fehler beim Laden der Lauf-Daten');
|
||||
@@ -699,7 +699,7 @@ async function deletePlayer(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/admin/players/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Spieler erfolgreich gelöscht');
|
||||
loadPlayers();
|
||||
@@ -718,7 +718,7 @@ async function deleteRun(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/admin/runs/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Lauf erfolgreich gelöscht');
|
||||
loadRuns();
|
||||
@@ -737,7 +737,7 @@ async function deleteLocation(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/admin/locations/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Standort erfolgreich gelöscht');
|
||||
loadLocations();
|
||||
@@ -756,7 +756,7 @@ async function deleteAdminUser(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/admin/adminusers/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Admin-Benutzer erfolgreich gelöscht');
|
||||
loadAdminUsers();
|
||||
@@ -774,12 +774,12 @@ function confirmDelete(message) {
|
||||
return new Promise((resolve) => {
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
document.getElementById('confirmModal').style.display = 'block';
|
||||
|
||||
|
||||
document.getElementById('confirmYes').onclick = () => {
|
||||
closeModal();
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
|
||||
document.getElementById('confirmNo').onclick = () => {
|
||||
closeModal();
|
||||
resolve(false);
|
||||
@@ -806,3 +806,493 @@ function showError(message) {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BLACKLIST MANAGEMENT FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
async function showBlacklistManagement() {
|
||||
currentDataType = 'blacklist';
|
||||
document.getElementById('blacklistModal').style.display = 'block';
|
||||
await loadBlacklist();
|
||||
await loadBlacklistStats();
|
||||
}
|
||||
|
||||
async function loadBlacklist() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/blacklist');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
displayBlacklist(result.data);
|
||||
} else {
|
||||
showBlacklistMessage('Fehler beim Laden der Blacklist: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading blacklist:', error);
|
||||
showBlacklistMessage('Fehler beim Laden der Blacklist', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function displayBlacklist(blacklist) {
|
||||
const content = document.getElementById('blacklistContent');
|
||||
|
||||
let html = '<h3 style="color: #ffffff; margin-bottom: 1rem;">📋 Blacklist-Inhalte</h3>';
|
||||
|
||||
Object.entries(blacklist).forEach(([category, terms]) => {
|
||||
const categoryName = getCategoryDisplayName(category);
|
||||
const categoryIcon = getCategoryIcon(category);
|
||||
|
||||
html += `
|
||||
<div style="border: 1px solid #334155; padding: 1rem; margin-bottom: 1rem; border-radius: 8px; background: rgba(15, 23, 42, 0.3);">
|
||||
<h4 style="color: #ffffff; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
${categoryIcon} ${categoryName}
|
||||
<span style="background: #1e293b; color: #8892b0; padding: 0.25rem 0.5rem; border-radius: 12px; font-size: 0.8rem;">
|
||||
${terms.length} Einträge
|
||||
</span>
|
||||
</h4>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
`;
|
||||
|
||||
if (terms.length === 0) {
|
||||
html += `
|
||||
<div style="color: #64748b; font-style: italic; padding: 1rem; text-align: center; width: 100%;">
|
||||
Keine Einträge in dieser Kategorie
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
terms.forEach(term => {
|
||||
html += `
|
||||
<span style="background: #1e293b; color: #e2e8f0; padding: 0.375rem 0.75rem; border-radius: 6px; display: flex; align-items: center; gap: 0.5rem; border: 1px solid #334155;">
|
||||
<span style="font-family: monospace; font-size: 0.9rem;">${term}</span>
|
||||
<button onclick="removeFromBlacklist('${term}', '${category}')"
|
||||
style="background: #dc2626; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s;"
|
||||
onmouseover="this.style.backgroundColor='#b91c1c'"
|
||||
onmouseout="this.style.backgroundColor='#dc2626'"
|
||||
title="Entfernen">
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
content.innerHTML = html;
|
||||
}
|
||||
|
||||
// Kategorie-Icons hinzufügen
|
||||
function getCategoryIcon(category) {
|
||||
const icons = {
|
||||
historical: '🏛️',
|
||||
offensive: '⚠️',
|
||||
titles: '👑',
|
||||
brands: '🏷️',
|
||||
inappropriate: '🚫'
|
||||
};
|
||||
return icons[category] || '📝';
|
||||
}
|
||||
|
||||
function getCategoryDisplayName(category) {
|
||||
const names = {
|
||||
historical: 'Historisch belastet',
|
||||
offensive: 'Beleidigend/anstößig',
|
||||
titles: 'Titel/Berufsbezeichnung',
|
||||
brands: 'Markenname',
|
||||
inappropriate: 'Unpassend'
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
async function testNameAgainstBlacklist() {
|
||||
const firstname = document.getElementById('testFirstname').value.trim();
|
||||
const lastname = document.getElementById('testLastname').value.trim();
|
||||
const resultDiv = document.getElementById('testResult');
|
||||
|
||||
if (!firstname || !lastname) {
|
||||
showBlacklistMessage('Bitte gib Vorname und Nachname ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/blacklist/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ firstname, lastname })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const testResult = result.data;
|
||||
|
||||
if (testResult.isBlocked) {
|
||||
let matchTypeText = '';
|
||||
let similarityText = '';
|
||||
|
||||
if (testResult.matchType === 'exact') {
|
||||
matchTypeText = 'Exakte Übereinstimmung';
|
||||
} else if (testResult.matchType === 'similar') {
|
||||
matchTypeText = 'Ähnliche Übereinstimmung (Levenshtein)';
|
||||
const similarityPercent = Math.round((1 - testResult.similarity) * 100);
|
||||
similarityText = `<br>Ähnlichkeit: ${similarityPercent}% (Distanz: ${testResult.levenshteinDistance})`;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div style="background: #ffebee; color: #c62828; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>❌ Name ist blockiert</strong><br>
|
||||
Grund: ${testResult.reason}<br>
|
||||
Kategorie: ${getCategoryDisplayName(testResult.category)}<br>
|
||||
Gefundener Begriff: "${testResult.matchedTerm}"<br>
|
||||
Typ: ${matchTypeText}${similarityText}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `
|
||||
<div style="background: #e8f5e8; color: #2e7d32; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>✅ Name ist erlaubt</strong><br>
|
||||
Der Name "${firstname} ${lastname}" ist nicht in der Blacklist.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
} else {
|
||||
showBlacklistMessage('Fehler beim Testen: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error testing name:', error);
|
||||
showBlacklistMessage('Fehler beim Testen des Namens', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function addToBlacklist() {
|
||||
const term = document.getElementById('newTerm').value.trim();
|
||||
const category = document.getElementById('newCategory').value;
|
||||
|
||||
if (!term) {
|
||||
showBlacklistMessage('Bitte gib einen Begriff ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/blacklist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ term, category })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showBlacklistMessage('Begriff erfolgreich hinzugefügt', 'success');
|
||||
document.getElementById('newTerm').value = '';
|
||||
await loadBlacklist();
|
||||
await loadBlacklistStats();
|
||||
} else {
|
||||
showBlacklistMessage('Fehler beim Hinzufügen: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding to blacklist:', error);
|
||||
showBlacklistMessage('Fehler beim Hinzufügen zur Blacklist', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFromBlacklist(term, category) {
|
||||
if (!confirm(`Möchtest du "${term}" aus der Kategorie "${getCategoryDisplayName(category)}" entfernen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/blacklist', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ term, category })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showBlacklistMessage('Begriff erfolgreich entfernt', 'success');
|
||||
await loadBlacklist();
|
||||
await loadBlacklistStats();
|
||||
} else {
|
||||
showBlacklistMessage('Fehler beim Entfernen: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing from blacklist:', error);
|
||||
showBlacklistMessage('Fehler beim Entfernen aus der Blacklist', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showBlacklistMessage(message, type) {
|
||||
const messageDiv = document.getElementById('blacklistMessage');
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.className = `message ${type}`;
|
||||
messageDiv.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Blacklist-Statistiken laden
|
||||
async function loadBlacklistStats() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/blacklist/stats');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
displayBlacklistStats(result.data);
|
||||
} else {
|
||||
console.error('Error loading blacklist stats:', result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading blacklist stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Blacklist-Statistiken anzeigen
|
||||
function displayBlacklistStats(stats) {
|
||||
// Erstelle oder aktualisiere Statistiken-Bereich
|
||||
let statsDiv = document.getElementById('blacklistStats');
|
||||
if (!statsDiv) {
|
||||
// Erstelle Statistiken-Bereich am Anfang des Modals
|
||||
const modalContent = document.querySelector('#blacklistModal .modal-content');
|
||||
const firstChild = modalContent.firstChild;
|
||||
statsDiv = document.createElement('div');
|
||||
statsDiv.id = 'blacklistStats';
|
||||
statsDiv.style.cssText = 'border: 1px solid #4ade80; padding: 1rem; margin-bottom: 1rem; border-radius: 5px; background: rgba(34, 197, 94, 0.1);';
|
||||
modalContent.insertBefore(statsDiv, firstChild);
|
||||
}
|
||||
|
||||
let html = `
|
||||
<h4 style="color: #4ade80; margin-bottom: 1rem;">📊 Blacklist-Statistiken</h4>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
|
||||
`;
|
||||
|
||||
stats.categories.forEach(category => {
|
||||
const categoryName = getCategoryDisplayName(category.category);
|
||||
const lastAdded = new Date(category.last_added).toLocaleDateString('de-DE');
|
||||
html += `
|
||||
<div style="background: rgba(15, 23, 42, 0.5); padding: 0.75rem; border-radius: 5px; text-align: center;">
|
||||
<div style="font-size: 1.5rem; font-weight: bold; color: #4ade80;">${category.count}</div>
|
||||
<div style="font-size: 0.9rem; color: #8892b0;">${categoryName}</div>
|
||||
<div style="font-size: 0.8rem; color: #64748b;">Letzte: ${lastAdded}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<div style="text-align: center; padding: 0.5rem; background: rgba(15, 23, 42, 0.3); border-radius: 5px;">
|
||||
<strong style="color: #4ade80;">Gesamt: ${stats.total} Begriffe</strong>
|
||||
</div>
|
||||
`;
|
||||
|
||||
statsDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
// LLM-Management
|
||||
function showLLMManagement() {
|
||||
document.getElementById('llmModal').style.display = 'block';
|
||||
loadLLMStatus();
|
||||
}
|
||||
|
||||
function closeLLMModal() {
|
||||
document.getElementById('llmModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// LLM-Status laden
|
||||
async function loadLLMStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/admin/llm/status', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const statusContent = document.getElementById('llmStatusContent');
|
||||
|
||||
if (result.success) {
|
||||
const status = result.data;
|
||||
if (status.connected) {
|
||||
statusContent.innerHTML = `
|
||||
<div style="background: #e8f5e8; color: #2e7d32; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>✅ KI-Moderator verbunden</strong><br>
|
||||
Modell: ${status.model}<br>
|
||||
URL: ${status.baseUrl}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
statusContent.innerHTML = `
|
||||
<div style="background: #ffebee; color: #c62828; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>❌ KI-Moderator nicht verfügbar</strong><br>
|
||||
Modell: ${status.model}<br>
|
||||
URL: ${status.baseUrl}<br>
|
||||
Fehler: ${status.error}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
statusContent.innerHTML = `
|
||||
<div style="background: #ffebee; color: #c62828; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>❌ Fehler beim Laden des Status</strong><br>
|
||||
${result.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading LLM status:', error);
|
||||
document.getElementById('llmStatusContent').innerHTML = `
|
||||
<div style="background: #ffebee; color: #c62828; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>❌ Fehler beim Laden des Status</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Name mit LLM testen
|
||||
async function testNameWithLLM() {
|
||||
const firstname = document.getElementById('llmFirstname').value.trim();
|
||||
const lastname = document.getElementById('llmLastname').value.trim();
|
||||
const context = document.getElementById('llmContext').value.trim();
|
||||
|
||||
if (!firstname || !lastname) {
|
||||
showLLMMessage('Bitte gib Vorname und Nachname ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// LLM-Test
|
||||
const llmResponse = await fetch('/api/v1/admin/llm/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
|
||||
},
|
||||
body: JSON.stringify({ firstname, lastname, context })
|
||||
});
|
||||
|
||||
const llmResult = await llmResponse.json();
|
||||
|
||||
// Blacklist-Test zum Vergleich
|
||||
const blacklistResponse = await fetch('/api/v1/admin/blacklist/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
|
||||
},
|
||||
body: JSON.stringify({ firstname, lastname })
|
||||
});
|
||||
|
||||
const blacklistResult = await blacklistResponse.json();
|
||||
|
||||
// Ergebnisse anzeigen
|
||||
displayLLMResults(llmResult, blacklistResult);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error testing name with LLM:', error);
|
||||
showLLMMessage('Fehler beim Testen: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// LLM-Ergebnisse anzeigen
|
||||
function displayLLMResults(llmResult, blacklistResult) {
|
||||
const resultDiv = document.getElementById('llmResult');
|
||||
const resultContent = document.getElementById('llmResultContent');
|
||||
const comparisonDiv = document.getElementById('llmComparison');
|
||||
const comparisonContent = document.getElementById('llmComparisonContent');
|
||||
|
||||
if (llmResult.success) {
|
||||
const llm = llmResult.data;
|
||||
|
||||
let llmStatus = '';
|
||||
if (llm.isBlocked) {
|
||||
llmStatus = `
|
||||
<div style="background: #ffebee; color: #c62828; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>❌ Name blockiert</strong><br>
|
||||
Grund: ${llm.reason}<br>
|
||||
Konfidenz: ${Math.round(llm.confidence * 100)}%<br>
|
||||
LLM-Antwort: "${llm.llmResponse}"<br>
|
||||
Typ: ${llm.matchType}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
llmStatus = `
|
||||
<div style="background: #e8f5e8; color: #2e7d32; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>✅ Name erlaubt</strong><br>
|
||||
Grund: ${llm.reason}<br>
|
||||
Konfidenz: ${Math.round(llm.confidence * 100)}%<br>
|
||||
LLM-Antwort: "${llm.llmResponse}"<br>
|
||||
Typ: ${llm.matchType}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
resultContent.innerHTML = llmStatus;
|
||||
resultDiv.style.display = 'block';
|
||||
|
||||
// Vergleich mit Blacklist
|
||||
if (blacklistResult.success) {
|
||||
const blacklist = blacklistResult.data;
|
||||
let comparisonStatus = '';
|
||||
|
||||
if (blacklist.combined.isBlocked) {
|
||||
comparisonStatus = `
|
||||
<div style="background: #fff3e0; color: #f57c00; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>⚠️ Name blockiert (${blacklist.combined.source})</strong><br>
|
||||
Grund: ${blacklist.combined.reason}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
comparisonStatus = `
|
||||
<div style="background: #e8f5e8; color: #2e7d32; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>✅ Name erlaubt</strong><br>
|
||||
Sowohl KI als auch Blacklist erlauben den Namen
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
comparisonContent.innerHTML = comparisonStatus;
|
||||
comparisonDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
} else {
|
||||
resultContent.innerHTML = `
|
||||
<div style="background: #ffebee; color: #c62828; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>❌ Fehler beim Testen</strong><br>
|
||||
${llmResult.message}
|
||||
</div>
|
||||
`;
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// LLM-Nachricht anzeigen
|
||||
function showLLMMessage(message, type) {
|
||||
const resultDiv = document.getElementById('llmResult');
|
||||
const resultContent = document.getElementById('llmResultContent');
|
||||
|
||||
const color = type === 'error' ? '#c62828' : '#2e7d32';
|
||||
const bgColor = type === 'error' ? '#ffebee' : '#e8f5e8';
|
||||
|
||||
resultContent.innerHTML = `
|
||||
<div style="background: ${bgColor}; color: ${color}; padding: 0.5rem; border-radius: 3px;">
|
||||
<strong>${type === 'error' ? '❌' : '✅'} ${message}</strong>
|
||||
</div>
|
||||
`;
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -242,6 +242,53 @@ async function showRFIDSettings() {
|
||||
openModal('rfidModal');
|
||||
// Reset scanner state
|
||||
stopQRScanner();
|
||||
|
||||
// Check if user is already linked and hide/show create player section
|
||||
await updateCreatePlayerSectionVisibility();
|
||||
}
|
||||
|
||||
// Update visibility of create player section based on link status
|
||||
async function updateCreatePlayerSectionVisibility() {
|
||||
const createPlayerSection = document.getElementById('createPlayerSection');
|
||||
|
||||
if (!currentUser) {
|
||||
// No user logged in - hide create player section
|
||||
if (createPlayerSection) {
|
||||
createPlayerSection.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if user has a linked player
|
||||
const response = await fetch(`/api/v1/public/user-player/${currentUser.id}?t=${Date.now()}`);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success && result.data && result.data.id) {
|
||||
// User is already linked - hide create player section
|
||||
if (createPlayerSection) {
|
||||
createPlayerSection.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// User is not linked - show create player section
|
||||
if (createPlayerSection) {
|
||||
createPlayerSection.style.display = 'block';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Error checking link status - show create player section as fallback
|
||||
if (createPlayerSection) {
|
||||
createPlayerSection.style.display = 'block';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking link status:', error);
|
||||
// Error occurred - show create player section as fallback
|
||||
if (createPlayerSection) {
|
||||
createPlayerSection.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check link status and load times
|
||||
@@ -448,6 +495,117 @@ async function linkManualRfid() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create new RFID player record
|
||||
async function createRfidPlayerRecord() {
|
||||
const rawUid = document.getElementById('manualRfidInput').value.trim();
|
||||
const firstname = document.getElementById('playerFirstname').value.trim();
|
||||
const lastname = document.getElementById('playerLastname').value.trim();
|
||||
const birthdate = document.getElementById('playerBirthdate').value;
|
||||
|
||||
// Validation
|
||||
if (!rawUid) {
|
||||
const inputErrorMsg = currentLanguage === 'de' ?
|
||||
'Bitte gib eine RFID UID ein' :
|
||||
'Please enter a RFID UID';
|
||||
showMessage('rfidMessage', inputErrorMsg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!firstname) {
|
||||
const inputErrorMsg = currentLanguage === 'de' ?
|
||||
'Bitte gib einen Vornamen ein' :
|
||||
'Please enter a first name';
|
||||
showMessage('rfidMessage', inputErrorMsg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastname) {
|
||||
const inputErrorMsg = currentLanguage === 'de' ?
|
||||
'Bitte gib einen Nachnamen ein' :
|
||||
'Please enter a last name';
|
||||
showMessage('rfidMessage', inputErrorMsg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!birthdate) {
|
||||
const inputErrorMsg = currentLanguage === 'de' ?
|
||||
'Bitte gib ein Geburtsdatum ein' :
|
||||
'Please enter a birth date';
|
||||
showMessage('rfidMessage', inputErrorMsg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Format the UID to match database format
|
||||
const formattedUid = formatRfidUid(rawUid);
|
||||
|
||||
const formattedMsg = currentLanguage === 'de' ?
|
||||
`Erstelle Spieler: ${firstname} ${lastname} (${formattedUid})` :
|
||||
`Creating player: ${firstname} ${lastname} (${formattedUid})`;
|
||||
showMessage('rfidMessage', formattedMsg, 'info');
|
||||
|
||||
// Create player record
|
||||
const response = await fetch('/api/v1/public/players/create-with-rfid', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
rfiduid: formattedUid,
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
birthdate: birthdate,
|
||||
supabase_user_id: currentUser?.id || null
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const successMsg = currentLanguage === 'de' ?
|
||||
`Spieler erfolgreich erstellt: ${result.data.firstname} ${result.data.lastname}` :
|
||||
`Player successfully created: ${result.data.firstname} ${result.data.lastname}`;
|
||||
showMessage('rfidMessage', successMsg, 'success');
|
||||
|
||||
// Clear form
|
||||
document.getElementById('manualRfidInput').value = '';
|
||||
document.getElementById('playerFirstname').value = '';
|
||||
document.getElementById('playerLastname').value = '';
|
||||
document.getElementById('playerBirthdate').value = '';
|
||||
|
||||
// Hide create player section since user is now linked
|
||||
const createPlayerSection = document.getElementById('createPlayerSection');
|
||||
if (createPlayerSection) {
|
||||
createPlayerSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Refresh times if user is linked
|
||||
if (currentUser) {
|
||||
await checkLinkStatusAndLoadTimes();
|
||||
}
|
||||
} else {
|
||||
let errorMsg = result.message;
|
||||
|
||||
// Erweitere Fehlermeldung um Levenshtein-Details falls vorhanden
|
||||
if (result.details) {
|
||||
if (result.details.matchType === 'similar') {
|
||||
const similarityPercent = Math.round((1 - result.details.similarity) * 100);
|
||||
errorMsg += `\n\nÄhnlichkeit: ${similarityPercent}% (Distanz: ${result.details.levenshteinDistance})`;
|
||||
}
|
||||
}
|
||||
|
||||
showMessage('rfidMessage', errorMsg, 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating RFID player record:', error);
|
||||
const errorMsg = currentLanguage === 'de' ?
|
||||
`Fehler beim Erstellen: ${error.message}` :
|
||||
`Error creating: ${error.message}`;
|
||||
showMessage('rfidMessage', errorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Link user by RFID UID (core function)
|
||||
async function linkUserByRfidUid(rfidUid) {
|
||||
if (!currentUser) {
|
||||
@@ -549,6 +707,51 @@ function showTimesNotLinked() {
|
||||
}
|
||||
}
|
||||
|
||||
// Show RFID linked info with help button
|
||||
function showRFIDLinkedInfo(playerData) {
|
||||
// Find the RFID card and update it
|
||||
const rfidCard = document.querySelector('.card[onclick="showRFIDSettings()"]');
|
||||
if (rfidCard) {
|
||||
const isGerman = currentLanguage === 'de';
|
||||
|
||||
rfidCard.innerHTML = `
|
||||
<h3>${isGerman ? '🏷️ RFID Verknüpft' : '🏷️ RFID Linked'}</h3>
|
||||
<div style="background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 0.5rem; padding: 1rem; margin: 1rem 0;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<span style="color: #10b981; font-weight: 600;">✅</span>
|
||||
<span style="color: #10b981; font-weight: 600;">
|
||||
${isGerman ? 'Erfolgreich verknüpft' : 'Successfully linked'}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size: 0.9rem; color: #8892b0;">
|
||||
<div><strong>${isGerman ? 'Spieler:' : 'Player:'}</strong> ${playerData.firstname} ${playerData.lastname}</div>
|
||||
<div><strong>RFID:</strong> <code style="background: rgba(255,255,255,0.1); padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: monospace;">${playerData.rfiduid}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="requestRFIDHelp()" style="margin-top: 1rem; font-size: 0.9rem; padding: 0.5rem 1rem;">
|
||||
${isGerman ? '❓ Hilfe anfordern' : '❓ Request Help'}
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Remove the onclick from the card since we don't want it to open the modal
|
||||
rfidCard.removeAttribute('onclick');
|
||||
rfidCard.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
// Request RFID help
|
||||
function requestRFIDHelp() {
|
||||
const isGerman = currentLanguage === 'de';
|
||||
const message = isGerman ?
|
||||
'Hilfe-Anfrage gesendet! Ein Administrator wird sich bei dir melden, um bei der RFID-Änderung zu helfen.' :
|
||||
'Help request sent! An administrator will contact you to help with the RFID change.';
|
||||
|
||||
alert(message);
|
||||
|
||||
// Here you could send a notification to admins or log the help request
|
||||
console.log('RFID help requested by user:', currentUser?.email);
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
function showTimesLoading() {
|
||||
document.getElementById('timesLoading').style.display = 'block';
|
||||
@@ -584,6 +787,9 @@ async function loadUserTimesSection(playerData) {
|
||||
// Display times
|
||||
displayUserTimes(times);
|
||||
|
||||
// Show RFID info and help button
|
||||
showRFIDLinkedInfo(playerData);
|
||||
|
||||
// Show the times display
|
||||
document.getElementById('timesLoading').style.display = 'none';
|
||||
document.getElementById('timesNotLinked').style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user