gemini enhancements
This commit is contained in:
1
.env
1
.env
@@ -13,6 +13,7 @@ JWT_SECRET=jif32r7fFEKMh75jrfIHNF8i5hnik9HToB7540VF3gaf
|
|||||||
|
|
||||||
# Port the application will run on
|
# Port the application will run on
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
HTTPS_PORT=3443
|
||||||
|
|
||||||
NODE_ENV=development # Set to 'production' in production environment
|
NODE_ENV=development # Set to 'production' in production environment
|
||||||
|
|
||||||
|
@@ -5,6 +5,82 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Admin-Bereich - Turnierverwaltung</title>
|
<title>Admin-Bereich - Turnierverwaltung</title>
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<style>
|
||||||
|
/* Tab Styling */
|
||||||
|
.tab-container {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
overflow: hidden; /* Contains the floated buttons */
|
||||||
|
}
|
||||||
|
.tab-container button {
|
||||||
|
background-color: inherit;
|
||||||
|
float: left;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 14px 16px;
|
||||||
|
transition: 0.3s;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 0; /* Override general button style */
|
||||||
|
margin-right: 0; /* Override general button style */
|
||||||
|
border-bottom: 3px solid transparent; /* For active indicator */
|
||||||
|
}
|
||||||
|
.tab-container button:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
border-bottom-color: #bbb;
|
||||||
|
}
|
||||||
|
.tab-container button.active {
|
||||||
|
background-color: #ccc;
|
||||||
|
border-bottom-color: #007bff; /* Highlight active tab */
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
/* Hide inactive tab content */
|
||||||
|
.tab-content.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust section margin for better spacing with tabs */
|
||||||
|
.content-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
#selected-tournament-context {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
#selected-tournament-header {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 0; /* Connects visually with tabs */
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-bottom: none; /* Remove bottom border to merge with tabs */
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
}
|
||||||
|
#selected-tournament-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure forms inside tabs don't have extra borders */
|
||||||
|
.tab-content form {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.tab-content table {
|
||||||
|
margin-top: 0; /* Remove extra top margin for tables in tabs */
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
@@ -38,67 +114,34 @@
|
|||||||
|
|
||||||
<section id="tournament-management" class="content-section">
|
<section id="tournament-management" class="content-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Turnierverwaltung</h2>
|
<h2>Turnierübersicht & Verwaltung</h2>
|
||||||
<button id="show-add-tournament-form">Neues Turnier hinzufügen</button>
|
<button id="show-add-tournament-form">Neues Turnier erstellen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="tournament-form" class="hidden">
|
<form id="tournament-form" class="hidden">
|
||||||
<h3 id="tournament-form-title">Neues Turnier hinzufügen</h3>
|
<h3 id="tournament-form-title">Neues Turnier erstellen</h3>
|
||||||
<input type="hidden" id="tournament-id"> <div>
|
<input type="hidden" id="tournament-id">
|
||||||
<label for="tournament-name">Name:</label>
|
<div><label for="tournament-name">Name:</label><input type="text" id="tournament-name" required></div>
|
||||||
<input type="text" id="tournament-name" required>
|
<div><label for="tournament-date">Datum:</label><input type="date" id="tournament-date"></div>
|
||||||
</div>
|
<div><label for="tournament-location">Ort:</label><input type="text" id="tournament-location"></div>
|
||||||
<div>
|
<div><label for="tournament-type">Turnier-Typ:</label><select id="tournament-type" required><option value="knockout">KO-System</option><option value="group">Gruppenphase</option></select></div>
|
||||||
<label for="tournament-date">Datum:</label>
|
<div><label for="tournament-game-type">Spiel-Typ (Punkte pro Satz):</label><select id="tournament-game-type" required><option value="11_points">11 Punkte</option><option value="21_points">21 Punkte</option></select></div>
|
||||||
<input type="date" id="tournament-date">
|
<div><label for="tournament-max-players">Max. Spieler (optional):</label><input type="number" id="tournament-max-players" min="0"></div>
|
||||||
</div>
|
<div><label for="tournament-status">Status:</label><select id="tournament-status"><option value="planned">Geplant</option><option value="running">Laufend</option><option value="finished">Beendet</option><option value="cancelled">Abgesagt</option></select></div>
|
||||||
<div>
|
<div><label for="tournament-description">Beschreibung:</label><textarea id="tournament-description"></textarea></div>
|
||||||
<label for="tournament-location">Ort:</label>
|
<button type="submit" id="save-tournament-button">Speichern</button>
|
||||||
<input type="text" id="tournament-location">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="tournament-type">Turnier-Typ:</label>
|
|
||||||
<select id="tournament-type" required>
|
|
||||||
<option value="knockout">KO-System</option>
|
|
||||||
<option value="group">Gruppenphase</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="tournament-game-type">Spiel-Typ (Punkte pro Satz):</label>
|
|
||||||
<select id="tournament-game-type" required>
|
|
||||||
<option value="11_points">11 Punkte</option>
|
|
||||||
<option value="21_points">21 Punkte</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="tournament-max-players">Max. Spieler (optional):</label>
|
|
||||||
<input type="number" id="tournament-max-players" min="0">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="tournament-status">Status:</label>
|
|
||||||
<select id="tournament-status">
|
|
||||||
<option value="planned">Geplant</option>
|
|
||||||
<option value="running">Laufend</option>
|
|
||||||
<option value="finished">Beendet</option>
|
|
||||||
<option value="cancelled">Abgesagt</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="tournament-description">Beschreibung:</label>
|
|
||||||
<textarea id="tournament-description"></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" id="save-tournament-button">Speichern</button>
|
|
||||||
<button type="button" id="cancel-tournament-button" class="secondary">Abbrechen</button>
|
<button type="button" id="cancel-tournament-button" class="secondary">Abbrechen</button>
|
||||||
<div id="tournament-form-error" class="error-message hidden"></div>
|
<div id="tournament-form-error" class="error-message hidden"></div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="tournament-list-message" class="message-area hidden"></div> <table id="tournament-table">
|
<div id="tournament-list-message" class="message-area hidden"></div>
|
||||||
|
|
||||||
|
<p>Wählen Sie ein Turnier aus der Liste aus, um es zu verwalten:</p>
|
||||||
|
<table id="tournament-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>Ort</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -109,197 +152,169 @@
|
|||||||
<p id="loading-tournaments" class="loading-indicator hidden">Lade Turniere...</p>
|
<p id="loading-tournaments" class="loading-indicator hidden">Lade Turniere...</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="player-management" class="content-section">
|
<section id="selected-tournament-context" class="hidden">
|
||||||
<div class="section-header">
|
|
||||||
<h2>Spielerverwaltung</h2>
|
|
||||||
<div>
|
|
||||||
<input type="text" id="player-search" placeholder="Spieler suchen..." style="width: auto; display: inline-block; margin-right: 10px;">
|
|
||||||
<button id="show-add-player-form">Neuen Spieler hinzufügen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="player-form" class="hidden">
|
<div id="selected-tournament-header">
|
||||||
<h3 id="player-form-title">Neuen Spieler hinzufügen</h3>
|
<h2 id="selected-tournament-context-name">Turnier: [Name]</h2>
|
||||||
<input type="hidden" id="player-id">
|
</div>
|
||||||
<div>
|
|
||||||
<label for="player-first-name">Vorname:</label>
|
|
||||||
<input type="text" id="player-first-name" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="player-last-name">Nachname:</label>
|
|
||||||
<input type="text" id="player-last-name" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="player-club">Verein:</label>
|
|
||||||
<input type="text" id="player-club">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="player-qttr">QTTR-Punkte:</label>
|
|
||||||
<input type="number" id="player-qttr" min="0">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="player-age-class">Altersklasse:</label>
|
|
||||||
<input type="text" id="player-age-class" placeholder="z.B. Herren A, U18">
|
|
||||||
</div>
|
|
||||||
<button type="submit" id="save-player-button">Speichern</button>
|
|
||||||
<button type="button" id="cancel-player-button" class="secondary">Abbrechen</button>
|
|
||||||
<div id="player-form-error" class="error-message hidden"></div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="player-list-message" class="message-area hidden"></div>
|
<div class="tab-container">
|
||||||
|
<button class="tab-button active" data-tab="info">Übersicht</button>
|
||||||
|
<button class="tab-button" data-tab="players">Teilnehmer</button>
|
||||||
|
<button class="tab-button" data-tab="matches">Spiele</button>
|
||||||
|
<button class="tab-button" data-tab="users">Benutzer</button> <button class="tab-button" data-tab="settings">Einstellungen</button> </div>
|
||||||
|
|
||||||
<table id="player-table">
|
<div id="tab-content-info" class="tab-content">
|
||||||
<thead>
|
<h3>Turnierdetails</h3>
|
||||||
<tr>
|
<p>Hier könnten allgemeine Turnierinformationen, Statistiken oder schnelle Aktionen angezeigt werden.</p>
|
||||||
<th>Nachname</th>
|
<p><strong>Status:</strong> <span id="info-tab-status"></span></p>
|
||||||
<th>Vorname</th>
|
<p><strong>Typ:</strong> <span id="info-tab-type"></span></p>
|
||||||
<th>Verein</th>
|
<p><strong>Ort:</strong> <span id="info-tab-location"></span></p>
|
||||||
<th>QTTR</th>
|
<p><strong>Beschreibung:</strong> <span id="info-tab-description"></span></p>
|
||||||
<th>Altersklasse</th>
|
<button id="edit-selected-tournament-button">Turnierdetails bearbeiten</button>
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="player-list">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p id="loading-players" class="loading-indicator hidden">Lade Spieler...</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="user-management" class="content-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Benutzerverwaltung</h2>
|
|
||||||
<button id="show-add-user-form">Neuen Benutzer hinzufügen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="user-form" class="hidden">
|
|
||||||
<h3 id="user-form-title">Neuen Benutzer hinzufügen</h3>
|
|
||||||
<input type="hidden" id="user-id">
|
|
||||||
<div>
|
|
||||||
<label for="user-username">Benutzername:</label>
|
|
||||||
<input type="text" id="user-username" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="user-password">Passwort:</label>
|
|
||||||
<input type="password" id="user-password">
|
|
||||||
<small>Beim Bearbeiten leer lassen, um das Passwort nicht zu ändern.</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="user-role">Rolle:</label>
|
|
||||||
<select id="user-role" required>
|
|
||||||
<option value="spectator">Zuschauer</option>
|
|
||||||
<option value="referee">Schiedsrichter</option>
|
|
||||||
<option value="admin">Admin</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" id="save-user-button">Speichern</button>
|
|
||||||
<button type="button" id="cancel-user-button" class="secondary">Abbrechen</button>
|
|
||||||
<div id="user-form-error" class="error-message hidden"></div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="user-list-message" class="message-area hidden"></div>
|
<div id="tab-content-players" class="tab-content hidden">
|
||||||
|
<div class="section-header">
|
||||||
<table id="user-table">
|
<h3>Turnierteilnehmer</h3>
|
||||||
<thead>
|
<div>
|
||||||
<tr>
|
<button id="show-add-player-form-tab">Neuen Spieler erstellen</button>
|
||||||
<th>Benutzername</th>
|
<button id="link-existing-player-button" disabled title="Funktion zum Verknüpfen existierender Spieler noch nicht implementiert">Existierenden Spieler hinzufügen</button>
|
||||||
<th>Rolle</th>
|
|
||||||
<th>Erstellt am</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="user-list">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p id="loading-users" class="loading-indicator hidden">Lade Benutzer...</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="match-management" class="content-section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Spielverwaltung & Ergebnisse</h2>
|
|
||||||
<div>
|
|
||||||
<label for="select-tournament-for-matches" style="margin-right: 10px;">Turnier anzeigen:</label>
|
|
||||||
<select id="select-tournament-for-matches">
|
|
||||||
<option value="">-- Turnier wählen --</option>
|
|
||||||
</select>
|
|
||||||
<button id="show-add-match-form" disabled>Neues Spiel hinzufügen</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="match-form" class="hidden">
|
<form id="player-form" class="hidden">
|
||||||
<h3 id="match-form-title">Neues Spiel hinzufügen</h3>
|
<h4 id="player-form-title">Neuen Spieler erstellen</h4>
|
||||||
<input type="hidden" id="match-id">
|
<input type="hidden" id="player-id"> <div><label for="player-first-name">Vorname:</label><input type="text" id="player-first-name" required></div>
|
||||||
<input type="hidden" id="match-form-tournament-id"> <div>
|
<div><label for="player-last-name">Nachname:</label><input type="text" id="player-last-name" required></div>
|
||||||
<label for="match-round">Runde:</label>
|
<div>
|
||||||
<input type="number" id="match-round" min="1">
|
<label for="player-gender">Geschlecht:</label>
|
||||||
</div>
|
<select id="player-gender" required>
|
||||||
<div>
|
<option value="">-- Bitte wählen --</option>
|
||||||
<label for="match-number-in-round">Spiel Nr. in Runde:</label>
|
<option value="m">Männlich</option>
|
||||||
<input type="number" id="match-number-in-round" min="1">
|
<option value="w">Weiblich</option>
|
||||||
</div>
|
<option value="d">Divers</option>
|
||||||
<div>
|
</select>
|
||||||
<label for="match-player1">Spieler 1:</label>
|
</div>
|
||||||
<select id="match-player1"><option value="">-- Spieler wählen --</option></select>
|
<div><label for="player-club">Verein:</label><input type="text" id="player-club"></div>
|
||||||
</div>
|
<div><label for="player-qttr">QTTR-Punkte:</label><input type="number" id="player-qttr" min="0"></div>
|
||||||
<div>
|
<div><label for="player-age-class">Altersklasse:</label><input type="text" id="player-age-class" placeholder="z.B. Herren A, U18"></div>
|
||||||
<label for="match-player2">Spieler 2:</label>
|
<button type="submit" id="save-player-button">Spieler erstellen</button>
|
||||||
<select id="match-player2"><option value="">-- Spieler wählen --</option></select>
|
<button type="button" id="cancel-player-button" class="secondary">Abbrechen</button>
|
||||||
</div>
|
<div id="player-form-error" class="error-message hidden"></div>
|
||||||
<div>
|
<p><small>Erstellt einen Spieler global. Das Hinzufügen zum Turnier muss separat erfolgen (noch nicht implementiert).</small></p>
|
||||||
<label for="match-scheduled-time">Geplante Zeit:</label>
|
</form>
|
||||||
<input type="datetime-local" id="match-scheduled-time">
|
|
||||||
</div>
|
<div id="tournament-player-list-message" class="message-area hidden"></div>
|
||||||
<div>
|
|
||||||
<label for="match-table-number">Tisch Nr.:</label>
|
<table id="tournament-player-table">
|
||||||
<input type="text" id="match-table-number">
|
<thead>
|
||||||
</div>
|
<tr>
|
||||||
<div>
|
<th>Nachname</th>
|
||||||
<label for="match-status">Status:</label>
|
<th>Vorname</th>
|
||||||
<select id="match-status">
|
<th>Geschlecht</th>
|
||||||
<option value="scheduled">Geplant</option>
|
<th>Verein</th>
|
||||||
<option value="ongoing">Laufend</option>
|
<th>QTTR</th>
|
||||||
<option value="finished">Beendet</option>
|
<th>Status</th> <th>Aktionen</th>
|
||||||
<option value="postponed">Verschoben</option>
|
</tr>
|
||||||
</select>
|
</thead>
|
||||||
|
<tbody id="tournament-player-list">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p id="loading-tournament-players" class="loading-indicator hidden">Lade Teilnehmer...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-content-matches" class="tab-content hidden">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>Spiele dieses Turniers</h3>
|
||||||
|
<div>
|
||||||
|
<button id="show-add-match-form">Neues Spiel hinzufügen</button>
|
||||||
|
<button id="generate-bracket-button" disabled title="Funktion noch nicht implementiert">Turnierbaum generieren</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" id="save-match-button">Speichern</button>
|
<form id="match-form" class="hidden">
|
||||||
<button type="button" id="cancel-match-button" class="secondary">Abbrechen</button>
|
<h4 id="match-form-title">Neues Spiel hinzufügen</h4>
|
||||||
<div id="match-form-error" class="error-message hidden"></div>
|
<input type="hidden" id="match-id">
|
||||||
</form>
|
<input type="hidden" id="match-form-tournament-id"> <div><label for="match-round">Runde:</label><input type="number" id="match-round" min="1"></div>
|
||||||
|
<div><label for="match-number-in-round">Spiel Nr. in Runde:</label><input type="number" id="match-number-in-round" min="1"></div>
|
||||||
|
<div><label for="match-player1">Spieler 1:</label><select id="match-player1"><option value="">-- Teilnehmer wählen --</option></select></div>
|
||||||
|
<div><label for="match-player2">Spieler 2:</label><select id="match-player2"><option value="">-- Teilnehmer wählen --</option></select></div>
|
||||||
|
<div><label for="match-scheduled-time">Geplante Zeit:</label><input type="datetime-local" id="match-scheduled-time"></div>
|
||||||
|
<div><label for="match-table-number">Tisch Nr.:</label><input type="text" id="match-table-number"></div>
|
||||||
|
<div><label for="match-status">Status:</label><select id="match-status"><option value="scheduled">Geplant</option><option value="ongoing">Laufend</option><option value="finished">Beendet</option><option value="postponed">Verschoben</option></select></div>
|
||||||
|
<button type="submit" id="save-match-button">Speichern</button>
|
||||||
|
<button type="button" id="cancel-match-button" class="secondary">Abbrechen</button>
|
||||||
|
<div id="match-form-error" class="error-message hidden"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div id="match-list-message" class="message-area hidden"></div>
|
<div id="match-list-message" class="message-area hidden"></div>
|
||||||
|
|
||||||
<table id="match-table">
|
<table id="match-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Runde</th>
|
<th>Runde</th>
|
||||||
<th>Spiel Nr.</th>
|
<th>Spiel Nr.</th>
|
||||||
<th>Spieler 1</th>
|
<th>Spieler 1</th>
|
||||||
<th>Spieler 2</th>
|
<th>Spieler 2</th>
|
||||||
<th>Ergebnis</th> <th>Status</th>
|
<th>Ergebnis</th>
|
||||||
<th>Geplant</th>
|
<th>Status</th>
|
||||||
<th>Tisch</th>
|
<th>Geplant</th>
|
||||||
<th>Aktionen</th>
|
<th>Tisch</th>
|
||||||
</tr>
|
<th>Aktionen</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody id="match-list">
|
</thead>
|
||||||
</tbody>
|
<tbody id="match-list">
|
||||||
</table>
|
</tbody>
|
||||||
<p id="loading-matches" class="loading-indicator hidden">Lade Spiele...</p>
|
</table>
|
||||||
<p id="select-tournament-prompt" class="">Bitte oben ein Turnier auswählen, um Spiele anzuzeigen.</p>
|
<p id="loading-matches" class="loading-indicator hidden">Lade Spiele...</p>
|
||||||
|
|
||||||
</section>
|
<div id="bracket-visualization-area" style="margin-top: 20px;">
|
||||||
|
<h4>Turnierbaum / Gruppen</h4>
|
||||||
|
<p><i>Visualisierung noch nicht implementiert.</i></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section id="statistics-section" class="content-section">
|
<div id="tab-content-users" class="tab-content hidden">
|
||||||
<h2>Statistiken</h2>
|
<div class="section-header">
|
||||||
<p>Anzeige von Statistiken (Spielergebnisse, Quoten etc.) wird hier implementiert.</p>
|
<h3>Benutzerverwaltung (Global)</h3>
|
||||||
</section>
|
<button id="show-add-user-form">Neuen Benutzer hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section id="settings-section" class="content-section">
|
<form id="user-form" class="hidden">
|
||||||
<h2>Einstellungen</h2>
|
<h4 id="user-form-title">Neuen Benutzer hinzufügen</h4>
|
||||||
<p>Einstellungen (z.B. Backup-Intervall) werden hier implementiert.</p>
|
<input type="hidden" id="user-id">
|
||||||
</section>
|
<div><label for="user-username">Benutzername:</label><input type="text" id="user-username" required></div>
|
||||||
|
<div><label for="user-password">Passwort:</label><input type="password" id="user-password"><small>Beim Bearbeiten leer lassen, um nicht zu ändern.</small></div>
|
||||||
|
<div><label for="user-role">Rolle:</label><select id="user-role" required><option value="spectator">Zuschauer</option><option value="referee">Schiedsrichter</option><option value="admin">Admin</option></select></div>
|
||||||
|
<button type="submit" id="save-user-button">Speichern</button>
|
||||||
|
<button type="button" id="cancel-user-button" class="secondary">Abbrechen</button>
|
||||||
|
<div id="user-form-error" class="error-message hidden"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
</div> </div> <script src="/js/admin.js"></script>
|
<div id="user-list-message" class="message-area hidden"></div>
|
||||||
|
|
||||||
|
<table id="user-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Benutzername</th>
|
||||||
|
<th>Rolle</th>
|
||||||
|
<th>Erstellt am</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="user-list">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p id="loading-users" class="loading-indicator hidden">Lade Benutzer...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-content-settings" class="tab-content hidden">
|
||||||
|
<h3>Einstellungen</h3>
|
||||||
|
<p>Globale oder turnierspezifische Einstellungen (z.B. Backup-Intervall, Standard-Spielregeln) könnten hier verwaltet werden.</p>
|
||||||
|
<p><i>Funktion noch nicht implementiert.</i></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section> </div> </div> <script src="/js/admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
1226
public/js/admin.js
1226
public/js/admin.js
File diff suppressed because it is too large
Load Diff
@@ -7,28 +7,20 @@
|
|||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
/* Zusätzliche Stile für die Ergebniseingabe */
|
/* Zusätzliche Stile für die Ergebniseingabe */
|
||||||
.set-score-row {
|
.set-score-row { display: flex; align-items: center; margin-bottom: 10px; gap: 10px; }
|
||||||
display: flex;
|
.set-score-row label { width: 50px; margin-bottom: 0; }
|
||||||
align-items: center;
|
.set-score-row input[type="number"] { width: 60px; margin-bottom: 0; text-align: center; }
|
||||||
margin-bottom: 10px;
|
.set-score-row span { margin: 0 5px; }
|
||||||
gap: 10px; /* Abstand zwischen Elementen */
|
#match-details-players { font-weight: bold; margin-bottom: 15px; }
|
||||||
|
/* Stile für die Match-Liste */
|
||||||
|
#match-list-referee .match-item {
|
||||||
|
border: 1px solid #eee; padding: 10px; margin-bottom: 10px; border-radius: 4px;
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
}
|
}
|
||||||
.set-score-row label {
|
#match-list-referee .match-item span { font-size: 0.9em; }
|
||||||
width: 50px; /* Feste Breite für "Satz X:" */
|
#match-list-referee .match-item button { padding: 5px 10px; font-size: 0.9em; }
|
||||||
margin-bottom: 0; /* Label neben Inputs */
|
#no-active-tournament-referee { font-weight: bold; color: #dc3545; }
|
||||||
}
|
|
||||||
.set-score-row input[type="number"] {
|
|
||||||
width: 60px; /* Schmalere Input-Felder für Punkte */
|
|
||||||
margin-bottom: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.set-score-row span {
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
#match-details-players {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -46,14 +38,8 @@
|
|||||||
<div id="login-section" class="auth-form">
|
<div id="login-section" class="auth-form">
|
||||||
<h2>Login (Schiedsrichter)</h2>
|
<h2>Login (Schiedsrichter)</h2>
|
||||||
<form id="login-form">
|
<form id="login-form">
|
||||||
<div>
|
<div><label for="login-username">Benutzername:</label><input type="text" id="login-username" required></div>
|
||||||
<label for="login-username">Benutzername:</label>
|
<div><label for="login-password">Passwort:</label><input type="password" id="login-password" required></div>
|
||||||
<input type="text" id="login-username" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="login-password">Passwort:</label>
|
|
||||||
<input type="password" id="login-password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit">Login</button>
|
<button type="submit">Login</button>
|
||||||
<div id="login-error" class="error-message hidden"></div>
|
<div id="login-error" class="error-message hidden"></div>
|
||||||
</form>
|
</form>
|
||||||
@@ -61,13 +47,14 @@
|
|||||||
|
|
||||||
<div id="referee-content" class="hidden">
|
<div id="referee-content" class="hidden">
|
||||||
<section class="content-section">
|
<section class="content-section">
|
||||||
<h2>Spiel auswählen & Ergebnisse eintragen</h2>
|
<h2>Aktuelles Turnier: <span id="active-tournament-name">Lädt...</span></h2>
|
||||||
|
<p id="no-active-tournament-referee" class="hidden">Derzeit ist kein Turnier als 'laufend' markiert.</p>
|
||||||
|
|
||||||
<div id="match-selection">
|
<div id="match-selection">
|
||||||
<label for="select-match">Aktives/Geplantes Spiel auswählen:</label>
|
<h3>Verfügbare Spiele (Geplant / Laufend)</h3>
|
||||||
<select id="select-match" disabled>
|
<div id="match-list-referee">
|
||||||
<option value="">-- Bitte Spiel wählen --</option>
|
<p><i>Wähle ein Spiel aus der Liste, um Ergebnisse einzutragen.</i></p>
|
||||||
</select>
|
</div>
|
||||||
<p id="loading-matches" class="loading-indicator hidden">Lade verfügbare Spiele...</p>
|
<p id="loading-matches" class="loading-indicator hidden">Lade verfügbare Spiele...</p>
|
||||||
<div id="match-load-error" class="message-area error-message hidden"></div>
|
<div id="match-load-error" class="message-area error-message hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,8 +63,7 @@
|
|||||||
<h3>Ergebnisse eintragen</h3>
|
<h3>Ergebnisse eintragen</h3>
|
||||||
<div id="match-details-players">Spieler: Lädt...</div>
|
<div id="match-details-players">Spieler: Lädt...</div>
|
||||||
<form id="score-form">
|
<form id="score-form">
|
||||||
<div id="set-inputs">
|
<div id="set-inputs"></div>
|
||||||
</div>
|
|
||||||
<div style="margin-top: 15px;">
|
<div style="margin-top: 15px;">
|
||||||
<button type="button" id="add-set-button">Weiteren Satz hinzufügen</button>
|
<button type="button" id="add-set-button">Weiteren Satz hinzufügen</button>
|
||||||
<button type="submit">Ergebnis speichern</button>
|
<button type="submit">Ergebnis speichern</button>
|
||||||
@@ -97,7 +83,9 @@
|
|||||||
const loginErrorRef = document.getElementById('login-error');
|
const loginErrorRef = document.getElementById('login-error');
|
||||||
const logoutButtonRef = document.getElementById('logout-button');
|
const logoutButtonRef = document.getElementById('logout-button');
|
||||||
const welcomeMessageRef = document.getElementById('welcome-message');
|
const welcomeMessageRef = document.getElementById('welcome-message');
|
||||||
const selectMatch = document.getElementById('select-match');
|
const activeTournamentNameSpan = document.getElementById('active-tournament-name');
|
||||||
|
const noActiveTournamentMessage = document.getElementById('no-active-tournament-referee');
|
||||||
|
const matchListRefereeDiv = document.getElementById('match-list-referee'); // Div to list matches
|
||||||
const loadingMatches = document.getElementById('loading-matches');
|
const loadingMatches = document.getElementById('loading-matches');
|
||||||
const matchLoadError = document.getElementById('match-load-error');
|
const matchLoadError = document.getElementById('match-load-error');
|
||||||
const scoreEntry = document.getElementById('score-entry');
|
const scoreEntry = document.getElementById('score-entry');
|
||||||
@@ -111,397 +99,238 @@
|
|||||||
// --- State ---
|
// --- State ---
|
||||||
let authTokenRef = localStorage.getItem('authToken');
|
let authTokenRef = localStorage.getItem('authToken');
|
||||||
let currentUserRef = JSON.parse(localStorage.getItem('currentUser'));
|
let currentUserRef = JSON.parse(localStorage.getItem('currentUser'));
|
||||||
let currentSelectedMatch = null; // Store details of the selected match
|
let currentSelectedMatch = null; // Store details of the selected match for scoring
|
||||||
|
let activeTournamentId = null; // Store the ID of the currently running tournament
|
||||||
|
|
||||||
// --- API Base URL ---
|
// --- API Base URL ---
|
||||||
const API_BASE_URL_REF = '/api';
|
const API_BASE_URL_REF = '/api';
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions (showMessageRef, hideMessageRef, setLoadingRef) ---
|
||||||
const showMessageRef = (element, message, isError = true, autohide = false) => {
|
const showMessageRef = (element, message, isError = true, autohide = false) => { if (!element) return; element.textContent = message; element.className = `message-area ${isError ? 'error-message' : 'success-message'}`; element.classList.remove('hidden'); if (autohide) { setTimeout(() => hideMessageRef(element), 5000); } };
|
||||||
if (!element) return;
|
const hideMessageRef = (element) => { if (element) { element.classList.add('hidden'); element.textContent = ''; } };
|
||||||
element.textContent = message;
|
const setLoadingRef = (element, isLoading) => { if (element) { element.classList.toggle('hidden', !isLoading); } };
|
||||||
element.className = `message-area ${isError ? 'error-message' : 'success-message'}`;
|
|
||||||
element.classList.remove('hidden');
|
|
||||||
if (autohide) {
|
|
||||||
setTimeout(() => hideMessageRef(element), 5000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideMessageRef = (element) => {
|
|
||||||
if (element) {
|
|
||||||
element.classList.add('hidden');
|
|
||||||
element.textContent = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLoadingRef = (element, isLoading) => {
|
|
||||||
if (element) {
|
|
||||||
element.classList.toggle('hidden', !isLoading);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- API Fetch Function ---
|
// --- API Fetch Function ---
|
||||||
const fetchAPIRef = async (endpoint, options = {}) => {
|
const fetchAPIRef = async (endpoint, options = {}) => {
|
||||||
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
||||||
if (authTokenRef) {
|
if (authTokenRef) { headers['Authorization'] = `Bearer ${authTokenRef}`; }
|
||||||
headers['Authorization'] = `Bearer ${authTokenRef}`;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL_REF}${endpoint}`, { ...options, headers });
|
const response = await fetch(`${API_BASE_URL_REF}${endpoint}`, { ...options, headers });
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) { logoutRef(); throw new Error('Authentifizierung fehlgeschlagen.'); }
|
||||||
logoutRef(); // Logout on auth error
|
if (!response.ok) { let e = { m: `HTTP-Fehler: ${response.status} ${response.statusText}` }; try { const p = await response.json(); if (p && p.message) e.m = p.message; } catch (err) {} throw new Error(e.m); }
|
||||||
throw new Error('Authentifizierung fehlgeschlagen oder Token abgelaufen.');
|
if (response.status === 204) return null;
|
||||||
}
|
|
||||||
if (!response.ok) {
|
|
||||||
let errorData = { message: `HTTP-Fehler: ${response.status} ${response.statusText}` };
|
|
||||||
try {
|
|
||||||
const parsedError = await response.json();
|
|
||||||
if (parsedError && parsedError.message) errorData.message = parsedError.message;
|
|
||||||
} catch (e) { /* Ignore parsing error */ }
|
|
||||||
throw new Error(errorData.message);
|
|
||||||
}
|
|
||||||
if (response.status === 204) return null; // Handle No Content
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) { console.error('API Fetch Error (Referee):', error); throw error; }
|
||||||
console.error('API Fetch Error (Referee):', error);
|
|
||||||
throw error; // Re-throw for handling in calling function
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Authentication ---
|
// --- Authentication ---
|
||||||
const handleLoginRef = async (event) => {
|
const handleLoginRef = async (event) => { /* ... (same as referee_html_v2) ... */
|
||||||
event.preventDefault();
|
event.preventDefault(); hideMessageRef(loginErrorRef);
|
||||||
hideMessageRef(loginErrorRef);
|
const username = loginUsernameInputRef.value.trim(); const password = loginPasswordInputRef.value.trim();
|
||||||
const username = loginUsernameInputRef.value.trim();
|
if (!username || !password) { showMessageRef(loginErrorRef, 'Bitte Benutzername und Passwort eingeben.'); return; }
|
||||||
const password = loginPasswordInputRef.value.trim();
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
showMessageRef(loginErrorRef, 'Bitte Benutzername und Passwort eingeben.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchAPIRef('/auth/login', {
|
const data = await fetchAPIRef('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) });
|
||||||
method: 'POST',
|
if (data.token && data.user && (data.user.role === 'referee' || data.user.role === 'admin')) {
|
||||||
body: JSON.stringify({ username, password }),
|
authTokenRef = data.token; currentUserRef = data.user;
|
||||||
});
|
localStorage.setItem('authToken', authTokenRef); localStorage.setItem('currentUser', JSON.stringify(currentUserRef));
|
||||||
|
updateUIRef(); loadActiveTournamentAndMatches(); loginFormRef.reset(); // Load data for active tournament
|
||||||
// Allow 'admin' or 'referee' role
|
|
||||||
if (data.token && data.user && (data.user.role === 'referee' || data.user.role === 'admin')) {
|
|
||||||
authTokenRef = data.token;
|
|
||||||
currentUserRef = data.user;
|
|
||||||
localStorage.setItem('authToken', authTokenRef);
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(currentUserRef));
|
|
||||||
updateUIRef();
|
|
||||||
loadRefereeMatches(); // Load matches for the referee
|
|
||||||
loginFormRef.reset();
|
|
||||||
} else if (data.user && data.user.role !== 'referee' && data.user.role !== 'admin') {
|
} else if (data.user && data.user.role !== 'referee' && data.user.role !== 'admin') {
|
||||||
showMessageRef(loginErrorRef, 'Zugriff verweigert. Nur Schiedsrichter oder Admins können sich hier anmelden.');
|
showMessageRef(loginErrorRef, 'Zugriff verweigert. Nur Schiedsrichter oder Admins.'); logoutRef();
|
||||||
logoutRef(); // Log out if wrong role but valid login somehow occurred
|
} else { showMessageRef(loginErrorRef, data.message || 'Login fehlgeschlagen.'); }
|
||||||
} else {
|
} catch (error) { showMessageRef(loginErrorRef, `Login fehlgeschlagen: ${error.message}`); }
|
||||||
showMessageRef(loginErrorRef, data.message || 'Login fehlgeschlagen.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showMessageRef(loginErrorRef, `Login fehlgeschlagen: ${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
const logoutRef = () => { /* ... (same as referee_html_v2) ... */
|
||||||
const logoutRef = () => {
|
authTokenRef = null; currentUserRef = null; activeTournamentId = null; currentSelectedMatch = null;
|
||||||
authTokenRef = null;
|
localStorage.removeItem('authToken'); localStorage.removeItem('currentUser');
|
||||||
currentUserRef = null;
|
updateUIRef(); matchListRefereeDiv.innerHTML = ''; scoreEntry.classList.add('hidden');
|
||||||
localStorage.removeItem('authToken');
|
activeTournamentNameSpan.textContent = 'Unbekannt'; noActiveTournamentMessage.classList.add('hidden');
|
||||||
localStorage.removeItem('currentUser');
|
|
||||||
updateUIRef();
|
|
||||||
// Clear referee specific data
|
|
||||||
selectMatch.innerHTML = '<option value="">-- Bitte Spiel wählen --</option>';
|
|
||||||
selectMatch.disabled = true;
|
|
||||||
scoreEntry.classList.add('hidden');
|
|
||||||
currentSelectedMatch = null;
|
|
||||||
};
|
};
|
||||||
|
const updateUIRef = () => { /* ... (same as referee_html_v2) ... */
|
||||||
const updateUIRef = () => {
|
|
||||||
const isAllowedUser = authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin');
|
const isAllowedUser = authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin');
|
||||||
loginSectionRef.classList.toggle('hidden', isAllowedUser);
|
loginSectionRef.classList.toggle('hidden', isAllowedUser); refereeContent.classList.toggle('hidden', !isAllowedUser);
|
||||||
refereeContent.classList.toggle('hidden', !isAllowedUser);
|
logoutButtonRef.classList.toggle('hidden', !isAllowedUser); welcomeMessageRef.classList.toggle('hidden', !isAllowedUser);
|
||||||
logoutButtonRef.classList.toggle('hidden', !isAllowedUser);
|
if (isAllowedUser) { welcomeMessageRef.textContent = `Willkommen, ${currentUserRef.username}! (${currentUserRef.role})`; }
|
||||||
welcomeMessageRef.classList.toggle('hidden', !isAllowedUser);
|
else { if (currentUserRef) { showMessageRef(loginErrorRef, 'Keine Berechtigung für diese Seite.'); } }
|
||||||
if (isAllowedUser) {
|
|
||||||
welcomeMessageRef.textContent = `Willkommen, ${currentUserRef.username}! (${currentUserRef.role})`;
|
|
||||||
} else {
|
|
||||||
if (currentUserRef) { // Logged in but wrong role
|
|
||||||
showMessageRef(loginErrorRef, 'Sie sind angemeldet, haben aber keine Schiedsrichter-Berechtigung für diese Seite.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Referee Specific Functions ---
|
// --- Referee Specific Functions ---
|
||||||
|
|
||||||
const loadRefereeMatches = async () => {
|
// Find the active tournament and load its matches
|
||||||
console.log("Lade Spiele für Schiedsrichter...");
|
const loadActiveTournamentAndMatches = async () => {
|
||||||
|
console.log("Suche aktives Turnier...");
|
||||||
setLoadingRef(loadingMatches, true);
|
setLoadingRef(loadingMatches, true);
|
||||||
selectMatch.disabled = true;
|
matchListRefereeDiv.innerHTML = ''; // Clear previous list
|
||||||
selectMatch.innerHTML = '<option value="">Lade Spiele...</option>';
|
|
||||||
hideMessageRef(matchLoadError);
|
hideMessageRef(matchLoadError);
|
||||||
scoreEntry.classList.add('hidden'); // Hide score entry while loading matches
|
noActiveTournamentMessage.classList.add('hidden');
|
||||||
currentSelectedMatch = null;
|
activeTournamentNameSpan.textContent = 'Lädt...';
|
||||||
|
activeTournamentId = null; // Reset active tournament ID
|
||||||
let allRelevantMatches = [];
|
scoreEntry.classList.add('hidden'); // Hide score entry initially
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Inefficient approach: Fetch all tournaments, then fetch relevant matches for each.
|
const tournaments = await fetchAPIRef('/tournaments');
|
||||||
// A dedicated backend endpoint would be much better.
|
const runningTournament = tournaments.find(t => t.status === 'running');
|
||||||
console.warn("Fetching all tournaments to find matches - consider optimizing backend.");
|
|
||||||
const tournaments = await fetchAPIRef('/tournaments'); // Assumes this endpoint is available
|
|
||||||
|
|
||||||
// Array to hold promises for fetching matches for each tournament
|
if (!runningTournament) {
|
||||||
const matchFetchPromises = tournaments.map(tournament =>
|
activeTournamentNameSpan.textContent = 'Kein Aktives';
|
||||||
fetchAPIRef(`/matches?tournamentId=${tournament.tournament_id}&status=scheduled`)
|
noActiveTournamentMessage.classList.remove('hidden');
|
||||||
.catch(e => { console.error(`Error fetching scheduled matches for ${tournament.name}:`, e); return []; }) // Ignore errors for single tournament fetch
|
setLoadingRef(loadingMatches, false);
|
||||||
);
|
return; // Stop if no tournament is running
|
||||||
const matchFetchPromisesOngoing = tournaments.map(tournament =>
|
|
||||||
fetchAPIRef(`/matches?tournamentId=${tournament.tournament_id}&status=ongoing`)
|
|
||||||
.catch(e => { console.error(`Error fetching ongoing matches for ${tournament.name}:`, e); return []; }) // Ignore errors for single tournament fetch
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// Wait for all fetches to complete
|
|
||||||
const resultsScheduled = await Promise.all(matchFetchPromises);
|
|
||||||
const resultsOngoing = await Promise.all(matchFetchPromisesOngoing);
|
|
||||||
|
|
||||||
// Flatten the results and add tournament name for display
|
|
||||||
resultsScheduled.forEach((matches, index) => {
|
|
||||||
matches.forEach(match => {
|
|
||||||
match.tournament_name = tournaments[index].name; // Add tournament name
|
|
||||||
allRelevantMatches.push(match);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
resultsOngoing.forEach((matches, index) => {
|
|
||||||
matches.forEach(match => {
|
|
||||||
match.tournament_name = tournaments[index].name; // Add tournament name
|
|
||||||
// Avoid duplicates if a match was somehow fetched twice
|
|
||||||
if (!allRelevantMatches.some(m => m.match_id === match.match_id)) {
|
|
||||||
allRelevantMatches.push(match);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Sort matches (e.g., by tournament, then round, then time)
|
|
||||||
allRelevantMatches.sort((a, b) => {
|
|
||||||
if (a.tournament_name < b.tournament_name) return -1;
|
|
||||||
if (a.tournament_name > b.tournament_name) return 1;
|
|
||||||
if ((a.round || 999) < (b.round || 999)) return -1; // Treat null rounds as last
|
|
||||||
if ((a.round || 999) > (b.round || 999)) return 1;
|
|
||||||
const timeA = a.scheduled_time ? new Date(a.scheduled_time).getTime() : Infinity;
|
|
||||||
const timeB = b.scheduled_time ? new Date(b.scheduled_time).getTime() : Infinity;
|
|
||||||
return timeA - timeB;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Populate dropdown
|
|
||||||
selectMatch.innerHTML = '<option value="">-- Bitte Spiel wählen --</option>'; // Reset
|
|
||||||
if (allRelevantMatches.length === 0) {
|
|
||||||
selectMatch.innerHTML += '<option value="" disabled>Keine aktiven/geplanten Spiele gefunden</option>';
|
|
||||||
} else {
|
|
||||||
allRelevantMatches.forEach(match => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = match.match_id;
|
|
||||||
const timeStr = match.scheduled_time ? `(${new Date(match.scheduled_time).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })})` : '';
|
|
||||||
option.textContent = `[${match.tournament_name}] Rd ${match.round || '?'} Tisch ${match.table_number || '?'} - ${match.player1_name || 'N/A'} vs ${match.player2_name || 'N/A'} ${timeStr}`;
|
|
||||||
selectMatch.appendChild(option);
|
|
||||||
});
|
|
||||||
selectMatch.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeTournamentId = runningTournament.tournament_id;
|
||||||
|
activeTournamentNameSpan.textContent = runningTournament.name;
|
||||||
|
console.log(`Aktives Turnier gefunden: ${runningTournament.name} (${activeTournamentId})`);
|
||||||
|
|
||||||
|
// Now load matches for this tournament
|
||||||
|
await loadRefereeMatches(activeTournamentId);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessageRef(matchLoadError, `Fehler beim Laden der Spiele: ${error.message}`);
|
showMessageRef(matchLoadError, `Fehler beim Laden des aktiven Turniers: ${error.message}`);
|
||||||
selectMatch.innerHTML = '<option value="" disabled>Fehler beim Laden</option>';
|
activeTournamentNameSpan.textContent = 'Fehler';
|
||||||
} finally {
|
setLoadingRef(loadingMatches, false);
|
||||||
setLoadingRef(loadingMatches, false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Display the score entry form for the selected match
|
// Load scheduled/ongoing matches for the given tournament ID
|
||||||
const displayScoreEntryForm = async (matchId) => {
|
const loadRefereeMatches = async (tournamentId) => {
|
||||||
console.log("Zeige Formular für Match ID:", matchId);
|
if (!tournamentId) return; // Should not happen if called correctly
|
||||||
hideMessageRef(scoreError);
|
setLoadingRef(loadingMatches, true); // Already set, but safe to repeat
|
||||||
hideMessageRef(scoreSuccess);
|
matchListRefereeDiv.innerHTML = ''; // Clear previous list
|
||||||
setInputs.innerHTML = '<div>Lade Spieldetails...</div>'; // Placeholder
|
hideMessageRef(matchLoadError);
|
||||||
scoreEntry.classList.remove('hidden');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch full match details, including existing sets and player names
|
// Fetch scheduled and ongoing matches for the active tournament
|
||||||
|
const scheduledPromise = fetchAPIRef(`/matches?tournamentId=${tournamentId}&status=scheduled`);
|
||||||
|
const ongoingPromise = fetchAPIRef(`/matches?tournamentId=${tournamentId}&status=ongoing`);
|
||||||
|
|
||||||
|
const [scheduledMatches, ongoingMatches] = await Promise.all([scheduledPromise, ongoingPromise]);
|
||||||
|
const allRelevantMatches = [...scheduledMatches, ...ongoingMatches];
|
||||||
|
|
||||||
|
// Sort matches (e.g., by round, then time)
|
||||||
|
allRelevantMatches.sort((a, b) => { /* ... sorting logic ... */
|
||||||
|
if ((a.round || 999) < (b.round || 999)) return -1; if ((a.round || 999) > (b.round || 999)) return 1;
|
||||||
|
const timeA = a.scheduled_time ? new Date(a.scheduled_time).getTime() : Infinity; const timeB = b.scheduled_time ? new Date(b.scheduled_time).getTime() : Infinity;
|
||||||
|
return timeA - timeB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render matches as list items with buttons
|
||||||
|
if (allRelevantMatches.length === 0) {
|
||||||
|
matchListRefereeDiv.innerHTML = '<p><i>Keine geplanten oder laufenden Spiele für dieses Turnier gefunden.</i></p>';
|
||||||
|
} else {
|
||||||
|
allRelevantMatches.forEach(match => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'match-item';
|
||||||
|
const timeStr = match.scheduled_time ? `(${new Date(match.scheduled_time).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })})` : '';
|
||||||
|
item.innerHTML = `
|
||||||
|
<span>
|
||||||
|
Rd ${match.round || '?'} Tisch ${match.table_number || '?'} - ${match.player1_name || 'N/A'} vs ${match.player2_name || 'N/A'} ${timeStr} [${match.status}]
|
||||||
|
</span>
|
||||||
|
<button class="score-match-button" data-match-id="${match.match_id}">Ergebnis eintragen</button>
|
||||||
|
`;
|
||||||
|
matchListRefereeDiv.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showMessageRef(matchLoadError, `Fehler beim Laden der Spiele: ${error.message}`);
|
||||||
|
matchListRefereeDiv.innerHTML = '<p><i>Fehler beim Laden der Spieleliste.</i></p>';
|
||||||
|
} finally {
|
||||||
|
setLoadingRef(loadingMatches, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display the score entry form (same as referee_html_v2)
|
||||||
|
const displayScoreEntryForm = async (matchId) => { /* ... (same as referee_html_v2) ... */
|
||||||
|
console.log("Zeige Formular für Match ID:", matchId);
|
||||||
|
hideMessageRef(scoreError); hideMessageRef(scoreSuccess);
|
||||||
|
setInputs.innerHTML = '<div>Lade Spieldetails...</div>';
|
||||||
|
scoreEntry.classList.remove('hidden');
|
||||||
|
currentSelectedMatch = null; // Reset selected match before loading
|
||||||
|
try {
|
||||||
currentSelectedMatch = await fetchAPIRef(`/matches/${matchId}`);
|
currentSelectedMatch = await fetchAPIRef(`/matches/${matchId}`);
|
||||||
if (!currentSelectedMatch) {
|
if (!currentSelectedMatch) { throw new Error("Spieldetails konnten nicht geladen werden."); }
|
||||||
throw new Error("Spieldetails konnten nicht geladen werden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
matchDetailsPlayers.textContent = `Spieler: ${currentSelectedMatch.player1_name || 'Spieler 1'} vs ${currentSelectedMatch.player2_name || 'Spieler 2'}`;
|
matchDetailsPlayers.textContent = `Spieler: ${currentSelectedMatch.player1_name || 'Spieler 1'} vs ${currentSelectedMatch.player2_name || 'Spieler 2'}`;
|
||||||
|
setInputs.innerHTML = '';
|
||||||
// Dynamically create input fields based on game type (best of 5 default) and existing scores
|
const maxSets = 5; const existingSets = currentSelectedMatch.sets || [];
|
||||||
setInputs.innerHTML = ''; // Clear placeholder/previous inputs
|
|
||||||
const maxSets = 5; // Assume best of 5 for now
|
|
||||||
const existingSets = currentSelectedMatch.sets || [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= maxSets; i++) {
|
for (let i = 1; i <= maxSets; i++) {
|
||||||
const existingSet = existingSets.find(s => s.set_number === i);
|
const existingSet = existingSets.find(s => s.set_number === i);
|
||||||
createSetInputRow(i, existingSet?.player1_score, existingSet?.player2_score);
|
createSetInputRow(i, existingSet?.player1_score, existingSet?.player2_score);
|
||||||
}
|
}
|
||||||
|
// Scroll to score entry
|
||||||
|
scoreEntry.scrollIntoView({ behavior: 'smooth' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessageRef(scoreError, `Fehler beim Laden der Spieldetails: ${error.message}`);
|
showMessageRef(scoreError, `Fehler beim Laden der Spieldetails: ${error.message}`);
|
||||||
matchDetailsPlayers.textContent = 'Fehler';
|
matchDetailsPlayers.textContent = 'Fehler'; setInputs.innerHTML = ''; currentSelectedMatch = null;
|
||||||
setInputs.innerHTML = ''; // Clear on error
|
|
||||||
currentSelectedMatch = null; // Reset selected match on error
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to create a row of set score inputs
|
// Helper to create a row of set score inputs (same as referee_html_v2)
|
||||||
const createSetInputRow = (setNumber, score1 = '', score2 = '') => {
|
const createSetInputRow = (setNumber, score1 = '', score2 = '') => { /* ... (same as referee_html_v2) ... */
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div'); div.className = 'set-score-row'; div.dataset.setNumber = setNumber;
|
||||||
div.className = 'set-score-row';
|
const label = document.createElement('label'); label.htmlFor = `set-${setNumber}-p1`; label.textContent = `Satz ${setNumber}:`;
|
||||||
div.dataset.setNumber = setNumber; // Store set number
|
const input1 = document.createElement('input'); input1.type = 'number'; input1.id = `set-${setNumber}-p1`; input1.min = "0"; input1.placeholder = "P1"; input1.value = score1; input1.dataset.player = "1";
|
||||||
|
const separator = document.createElement('span'); separator.textContent = "-";
|
||||||
const label = document.createElement('label');
|
const input2 = document.createElement('input'); input2.type = 'number'; input2.id = `set-${setNumber}-p2`; input2.min = "0"; input2.placeholder = "P2"; input2.value = score2; input2.dataset.player = "2";
|
||||||
label.htmlFor = `set-${setNumber}-p1`;
|
div.appendChild(label); div.appendChild(input1); div.appendChild(separator); div.appendChild(input2); setInputs.appendChild(div);
|
||||||
label.textContent = `Satz ${setNumber}:`;
|
|
||||||
|
|
||||||
const input1 = document.createElement('input');
|
|
||||||
input1.type = 'number';
|
|
||||||
input1.id = `set-${setNumber}-p1`;
|
|
||||||
input1.min = "0";
|
|
||||||
input1.placeholder = "P1";
|
|
||||||
input1.value = score1; // Pre-fill if score exists
|
|
||||||
input1.dataset.player = "1";
|
|
||||||
|
|
||||||
const separator = document.createElement('span');
|
|
||||||
separator.textContent = "-";
|
|
||||||
|
|
||||||
const input2 = document.createElement('input');
|
|
||||||
input2.type = 'number';
|
|
||||||
input2.id = `set-${setNumber}-p2`;
|
|
||||||
input2.min = "0";
|
|
||||||
input2.placeholder = "P2";
|
|
||||||
input2.value = score2; // Pre-fill if score exists
|
|
||||||
input2.dataset.player = "2";
|
|
||||||
|
|
||||||
div.appendChild(label);
|
|
||||||
div.appendChild(input1);
|
|
||||||
div.appendChild(separator);
|
|
||||||
div.appendChild(input2);
|
|
||||||
setInputs.appendChild(div);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle adding another set input row (same as referee_html_v2)
|
||||||
|
const handleAddSet = () => { /* ... (same as referee_html_v2) ... */
|
||||||
|
const currentSetCount = setInputs.children.length; createSetInputRow(currentSetCount + 1);
|
||||||
|
};
|
||||||
|
|
||||||
// Handle adding another set input row
|
// Handle saving the scores (same as referee_html_v2)
|
||||||
const handleAddSet = () => {
|
const handleSaveScore = async (event) => { /* ... (same as referee_html_v2, maybe refresh list on success) ... */
|
||||||
const currentSetCount = setInputs.children.length;
|
|
||||||
createSetInputRow(currentSetCount + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle saving the scores
|
|
||||||
const handleSaveScore = async (event) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const matchId = selectMatch.value;
|
// Use currentSelectedMatch.match_id instead of selectMatch.value
|
||||||
if (!matchId || !currentSelectedMatch) {
|
const matchId = currentSelectedMatch?.match_id;
|
||||||
showMessageRef(scoreError, "Kein gültiges Spiel ausgewählt.");
|
if (!matchId) { showMessageRef(scoreError, "Kein gültiges Spiel ausgewählt."); return; }
|
||||||
return;
|
hideMessageRef(scoreError); hideMessageRef(scoreSuccess);
|
||||||
}
|
const setsPayload = []; let formIsValid = true;
|
||||||
|
|
||||||
hideMessageRef(scoreError);
|
|
||||||
hideMessageRef(scoreSuccess);
|
|
||||||
|
|
||||||
// Collect scores from input fields
|
|
||||||
const setsPayload = [];
|
|
||||||
const setRows = setInputs.querySelectorAll('.set-score-row');
|
const setRows = setInputs.querySelectorAll('.set-score-row');
|
||||||
let formIsValid = true;
|
setRows.forEach((row) => { /* ... validation logic ... */
|
||||||
|
const setNumber = parseInt(row.dataset.setNumber); const inputs = row.querySelectorAll('input[type="number"]');
|
||||||
setRows.forEach((row) => {
|
const score1Input = inputs[0]; const score2Input = inputs[1];
|
||||||
const setNumber = parseInt(row.dataset.setNumber);
|
const score1 = score1Input.value.trim(); const score2 = score2Input.value.trim();
|
||||||
const inputs = row.querySelectorAll('input[type="number"]');
|
score1Input.style.borderColor = ''; score2Input.style.borderColor = ''; // Reset border
|
||||||
const score1Input = inputs[0];
|
|
||||||
const score2Input = inputs[1];
|
|
||||||
const score1 = score1Input.value.trim();
|
|
||||||
const score2 = score2Input.value.trim();
|
|
||||||
|
|
||||||
// Add scores only if both are entered and valid numbers
|
|
||||||
if (score1 !== '' && score2 !== '') {
|
if (score1 !== '' && score2 !== '') {
|
||||||
const p1Score = parseInt(score1);
|
const p1Score = parseInt(score1); const p2Score = parseInt(score2);
|
||||||
const p2Score = parseInt(score2);
|
if (!isNaN(p1Score) && !isNaN(p2Score) && p1Score >= 0 && p2Score >= 0) { setsPayload.push({ set_number: setNumber, player1_score: p1Score, player2_score: p2Score }); }
|
||||||
if (!isNaN(p1Score) && !isNaN(p2Score) && p1Score >= 0 && p2Score >= 0) {
|
else { showMessageRef(scoreError, `Ungültige Eingabe in Satz ${setNumber}.`); formIsValid = false; score1Input.style.borderColor = 'red'; score2Input.style.borderColor = 'red'; }
|
||||||
setsPayload.push({ set_number: setNumber, player1_score: p1Score, player2_score: p2Score });
|
} else if (score1 !== '' || score2 !== '') { showMessageRef(scoreError, `Bitte beide Punktzahlen für Satz ${setNumber} eingeben.`); formIsValid = false; score1Input.style.borderColor = 'red'; score2Input.style.borderColor = 'red'; }
|
||||||
} else {
|
|
||||||
showMessageRef(scoreError, `Ungültige Eingabe in Satz ${setNumber}. Bitte nur positive Zahlen eingeben.`);
|
|
||||||
formIsValid = false;
|
|
||||||
// Highlight invalid inputs (optional)
|
|
||||||
score1Input.style.borderColor = 'red';
|
|
||||||
score2Input.style.borderColor = 'red';
|
|
||||||
}
|
|
||||||
} else if (score1 !== '' || score2 !== '') {
|
|
||||||
// Only one score entered for the set
|
|
||||||
showMessageRef(scoreError, `Bitte beide Punktzahlen für Satz ${setNumber} eingeben oder beide leer lassen.`);
|
|
||||||
formIsValid = false;
|
|
||||||
score1Input.style.borderColor = 'red';
|
|
||||||
score2Input.style.borderColor = 'red';
|
|
||||||
} else {
|
|
||||||
// Both empty, reset border (optional)
|
|
||||||
score1Input.style.borderColor = '';
|
|
||||||
score2Input.style.borderColor = '';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
if (!formIsValid) return;
|
||||||
if (!formIsValid) {
|
if (setsPayload.length === 0) { showMessageRef(scoreError, "Keine gültigen Satzergebnisse zum Speichern."); return; }
|
||||||
return; // Stop if validation failed
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setsPayload.length === 0) {
|
|
||||||
showMessageRef(scoreError, "Keine gültigen Satzergebnisse zum Speichern eingegeben.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Speichere Ergebnis für Match", matchId, setsPayload);
|
console.log("Speichere Ergebnis für Match", matchId, setsPayload);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call the API endpoint to update scores
|
const result = await fetchAPIRef(`/matches/${matchId}/score`, { method: 'POST', body: JSON.stringify({ sets: setsPayload }) });
|
||||||
const result = await fetchAPIRef(`/matches/${matchId}/score`, {
|
showMessageRef(scoreSuccess, 'Ergebnis erfolgreich gespeichert.', false, true);
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ sets: setsPayload })
|
|
||||||
});
|
|
||||||
|
|
||||||
showMessageRef(scoreSuccess, 'Ergebnis erfolgreich gespeichert.', false, true); // Autohide success message
|
|
||||||
// Update the displayed form with potentially recalculated winner/status from response
|
|
||||||
if (result && result.match) {
|
if (result && result.match) {
|
||||||
currentSelectedMatch = result.match; // Update local match data
|
// Update local match data and redisplay form
|
||||||
// Re-display the form with updated data (especially if winner/status changed)
|
currentSelectedMatch = result.match;
|
||||||
matchDetailsPlayers.textContent = `Spieler: ${currentSelectedMatch.player1_name || 'Spieler 1'} vs ${currentSelectedMatch.player2_name || 'Spieler 2'}`;
|
matchDetailsPlayers.textContent = `Spieler: ${currentSelectedMatch.player1_name || 'Spieler 1'} vs ${currentSelectedMatch.player2_name || 'Spieler 2'}`;
|
||||||
setInputs.innerHTML = ''; // Clear old inputs
|
setInputs.innerHTML = ''; const maxSets = 5; const existingSets = currentSelectedMatch.sets || [];
|
||||||
const maxSets = 5;
|
for (let i = 1; i <= maxSets; i++) { const es = existingSets.find(s => s.set_number === i); createSetInputRow(i, es?.player1_score, es?.player2_score); }
|
||||||
const existingSets = currentSelectedMatch.sets || [];
|
// Refresh the match list as the status might have changed
|
||||||
for (let i = 1; i <= maxSets; i++) {
|
if (activeTournamentId) {
|
||||||
const existingSet = existingSets.find(s => s.set_number === i);
|
loadRefereeMatches(activeTournamentId);
|
||||||
createSetInputRow(i, existingSet?.player1_score, existingSet?.player2_score);
|
|
||||||
}
|
}
|
||||||
// Optionally update the match list dropdown if status changed
|
|
||||||
// loadRefereeMatches(); // Could reload the whole list, maybe too much?
|
|
||||||
}
|
}
|
||||||
|
} catch (error) { showMessageRef(scoreError, `Fehler beim Speichern: ${error.message}`); }
|
||||||
|
};
|
||||||
} catch (error) {
|
|
||||||
showMessageRef(scoreError, `Fehler beim Speichern: ${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// --- Event Listeners ---
|
// --- Event Listeners ---
|
||||||
loginFormRef.addEventListener('submit', handleLoginRef);
|
loginFormRef.addEventListener('submit', handleLoginRef);
|
||||||
logoutButtonRef.addEventListener('click', logoutRef);
|
logoutButtonRef.addEventListener('click', logoutRef);
|
||||||
|
|
||||||
selectMatch.addEventListener('change', (event) => {
|
// Event delegation for match selection buttons
|
||||||
const selectedMatchId = event.target.value;
|
matchListRefereeDiv.addEventListener('click', (event) => {
|
||||||
if (selectedMatchId) {
|
if (event.target.classList.contains('score-match-button')) {
|
||||||
displayScoreEntryForm(selectedMatchId);
|
const matchId = event.target.dataset.matchId;
|
||||||
} else {
|
if (matchId) {
|
||||||
scoreEntry.classList.add('hidden'); // Hide form if default option selected
|
displayScoreEntryForm(matchId);
|
||||||
currentSelectedMatch = null;
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -513,7 +342,7 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
updateUIRef();
|
updateUIRef();
|
||||||
if (authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin')) {
|
if (authTokenRef && currentUserRef && (currentUserRef.role === 'referee' || currentUserRef.role === 'admin')) {
|
||||||
loadRefereeMatches();
|
loadActiveTournamentAndMatches(); // Load data for active tournament
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -7,397 +7,256 @@
|
|||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<style>
|
<style>
|
||||||
/* Optional: Zusätzliche Stile für die Zuschaueransicht */
|
/* Optional: Zusätzliche Stile für die Zuschaueransicht */
|
||||||
.spectator-section h2 {
|
.spectator-section h2 { border-bottom: 2px solid #007bff; padding-bottom: 5px; margin-bottom: 15px; }
|
||||||
border-bottom: 2px solid #007bff;
|
.match-card { border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-bottom: 15px; background-color: #f8f9fa; }
|
||||||
padding-bottom: 5px;
|
.match-card .players { font-size: 1.1em; font-weight: bold; margin-bottom: 10px; }
|
||||||
margin-bottom: 15px;
|
.match-card .details { font-size: 0.9em; color: #555; }
|
||||||
}
|
.match-card .score { font-size: 1.2em; font-weight: bold; text-align: center; margin: 10px 0; }
|
||||||
.match-card { /* Beispiel für Live-Spiel-Anzeige */
|
#player-list table th, #player-list table td { padding: 8px; }
|
||||||
border: 1px solid #ddd;
|
footer { margin-top: 30px; padding-top: 15px; border-top: 1px solid #ccc; text-align: center; font-size: 0.9em; color: #666; }
|
||||||
border-radius: 5px;
|
footer a { color: #666; }
|
||||||
padding: 15px;
|
#no-active-tournament-spectator { font-weight: bold; color: #17a2b8; text-align: center; padding: 20px; background-color: #e9ecef; border-radius: 5px;}
|
||||||
margin-bottom: 15px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
.match-card .players {
|
|
||||||
font-size: 1.1em;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.match-card .details {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
.match-card .score { /* Für Live-Score später */
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
#player-list table th, #player-list table td {
|
|
||||||
padding: 8px; /* Etwas kompakter für Spielerliste */
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/admin.html">Admin</a>
|
|
||||||
<a href="/referee.html">Schiedsrichter</a>
|
<a href="/referee.html">Schiedsrichter</a>
|
||||||
<a href="/spectator.html" class="active">Zuschauer</a>
|
<a href="/spectator.html" class="active">Zuschauer</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Turnierübersicht</h1>
|
<h1>Turnierübersicht</h1>
|
||||||
|
<p id="no-active-tournament-spectator" class="hidden">Derzeit findet kein aktives Turnier statt.</p>
|
||||||
|
|
||||||
<section class="content-section spectator-section">
|
<section id="tournament-info-header" class="content-section spectator-section hidden" style="padding-bottom: 0; margin-bottom: 0;">
|
||||||
<h2>Turnier auswählen</h2>
|
<div id="tournament-info" style="padding: 10px 10px 15px 10px; background-color: #e9ecef; border-radius: 5px;">
|
||||||
<label for="select-tournament">Aktuelles/Vergangenes Turnier:</label>
|
<h2 id="selected-tournament-name" style="margin: 0 0 5px 0; border-bottom: none;">Turniername</h2>
|
||||||
<select id="select-tournament" disabled>
|
|
||||||
<option value="">-- Bitte Turnier wählen --</option>
|
|
||||||
</select>
|
|
||||||
<p id="loading-tournaments-spectator" class="loading-indicator hidden">Lade Turniere...</p>
|
|
||||||
<div id="tournament-info" class="hidden" style="margin-top: 15px; padding: 10px; background-color: #e9ecef; border-radius: 5px;">
|
|
||||||
<h3 id="selected-tournament-name" style="margin: 0 0 5px 0;"></h3>
|
|
||||||
<p id="selected-tournament-details" style="margin: 0; font-size: 0.9em;"></p>
|
<p id="selected-tournament-details" style="margin: 0; font-size: 0.9em;"></p>
|
||||||
<p id="selected-tournament-description" style="margin: 5px 0 0 0; font-size: 0.9em;"></p>
|
<p id="selected-tournament-description" style="margin: 5px 0 0 0; font-size: 0.9em;"></p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div id="tournament-display" class="hidden">
|
<div id="tournament-display" class="hidden">
|
||||||
|
|
||||||
<section class="content-section spectator-section">
|
<section class="content-section spectator-section">
|
||||||
<h2><span class="live-indicator" style="color: red;">●</span> Live Spiele / Aktuelle Runde</h2>
|
<h2><span class="live-indicator" style="color: red;">●</span> Live Spiele</h2>
|
||||||
<div id="live-matches-list">
|
<div id="live-matches-list"><p><i>Keine Live-Spiele gefunden.</i></p></div>
|
||||||
<p><i>Keine Live-Spiele gefunden oder Turnier nicht ausgewählt.</i></p>
|
|
||||||
</div>
|
|
||||||
<p id="loading-live-matches" class="loading-indicator hidden">Lade Live-Spiele...</p>
|
<p id="loading-live-matches" class="loading-indicator hidden">Lade Live-Spiele...</p>
|
||||||
<div id="live-matches-error" class="message-area error-message hidden"></div>
|
<div id="live-matches-error" class="message-area error-message hidden"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="content-section spectator-section">
|
<section class="content-section spectator-section">
|
||||||
<h2>Turnierbaum / Gruppenphase</h2>
|
<h2>Turnierbaum / Gruppenphase</h2>
|
||||||
<div id="bracket-groups-visualization">
|
<div id="bracket-groups-visualization"><p><i>Visualisierung noch nicht implementiert.</i></p></div>
|
||||||
<p><i>Visualisierung des Turnierbaums oder der Gruppenphase ist für dieses Turnier nicht verfügbar oder noch nicht implementiert.</i></p>
|
|
||||||
</div>
|
|
||||||
<p id="loading-bracket" class="loading-indicator hidden">Lade Turnierbaum/Gruppen...</p>
|
<p id="loading-bracket" class="loading-indicator hidden">Lade Turnierbaum/Gruppen...</p>
|
||||||
<div id="bracket-error" class="message-area error-message hidden"></div>
|
<div id="bracket-error" class="message-area error-message hidden"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="content-section spectator-section">
|
<section class="content-section spectator-section">
|
||||||
<h2>Alle Spiele (Zeitplan)</h2>
|
<h2>Alle Spiele (Zeitplan)</h2>
|
||||||
<div id="all-matches-list">
|
<div id="all-matches-list"><p><i>Keine Spiele gefunden.</i></p></div>
|
||||||
<p><i>Keine Spiele gefunden oder Turnier nicht ausgewählt.</i></p>
|
|
||||||
</div>
|
|
||||||
<p id="loading-all-matches" class="loading-indicator hidden">Lade alle Spiele...</p>
|
<p id="loading-all-matches" class="loading-indicator hidden">Lade alle Spiele...</p>
|
||||||
<div id="all-matches-error" class="message-area error-message hidden"></div>
|
<div id="all-matches-error" class="message-area error-message hidden"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="content-section spectator-section">
|
<section class="content-section spectator-section">
|
||||||
<h2>Spieler</h2>
|
<h2>Teilnehmer dieses Turniers</h2>
|
||||||
<div>
|
<div>
|
||||||
<label for="filter-player">Spieler suchen:</label>
|
<label for="filter-player">Spieler suchen:</label>
|
||||||
<input type="text" id="filter-player" placeholder="Namen oder Verein eingeben...">
|
<input type="text" id="filter-player" placeholder="Namen oder Verein eingeben...">
|
||||||
<small style="display: block; margin-top: 5px;"><i>Hinweis: Zeigt aktuell alle Spieler im System, nicht nur die des ausgewählten Turniers.</i></small>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="player-list" style="margin-top: 15px;">
|
<div id="player-list" style="margin-top: 15px;"><p><i>Keine Teilnehmer gefunden.</i></p></div>
|
||||||
<p><i>Keine Spieler gefunden.</i></p>
|
<p id="loading-players" class="loading-indicator hidden">Lade Teilnehmer...</p>
|
||||||
</div>
|
|
||||||
<p id="loading-players" class="loading-indicator hidden">Lade Spieler...</p>
|
|
||||||
<div id="player-list-error" class="message-area error-message hidden"></div>
|
<div id="player-list-error" class="message-area error-message hidden"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="content-section spectator-section">
|
<section class="content-section spectator-section">
|
||||||
<h2>Hinweise & Benachrichtigungen</h2>
|
<h2>Hinweise & Benachrichtigungen</h2>
|
||||||
<div id="notifications-area">
|
<div id="notifications-area"><p><i>Keine aktuellen Hinweise oder Funktion noch nicht implementiert.</i></p></div>
|
||||||
<p><i>Keine aktuellen Hinweise für dieses Turnier vorhanden oder Funktion noch nicht implementiert.</i></p>
|
|
||||||
</div>
|
|
||||||
<p id="loading-notifications" class="loading-indicator hidden">Lade Hinweise...</p>
|
<p id="loading-notifications" class="loading-indicator hidden">Lade Hinweise...</p>
|
||||||
<div id="notifications-error" class="message-area error-message hidden"></div>
|
<div id="notifications-error" class="message-area error-message hidden"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div> <div id="spectator-error" class="message-area error-message hidden"></div> </div> <script>
|
</div> <div id="spectator-error" class="message-area error-message hidden"></div> </div> <footer>
|
||||||
|
<a href="/admin.html">Admin-Bereich</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
// --- DOM Elements ---
|
// --- DOM Elements ---
|
||||||
const selectTournament = document.getElementById('select-tournament');
|
const spectatorError = document.getElementById('spectator-error');
|
||||||
const loadingTournamentsSpectator = document.getElementById('loading-tournaments-spectator');
|
const noActiveTournamentMessageSpec = document.getElementById('no-active-tournament-spectator');
|
||||||
|
const tournamentInfoHeader = document.getElementById('tournament-info-header');
|
||||||
const tournamentInfo = document.getElementById('tournament-info');
|
const tournamentInfo = document.getElementById('tournament-info');
|
||||||
const selectedTournamentName = document.getElementById('selected-tournament-name');
|
const selectedTournamentName = document.getElementById('selected-tournament-name');
|
||||||
const selectedTournamentDetails = document.getElementById('selected-tournament-details');
|
const selectedTournamentDetails = document.getElementById('selected-tournament-details');
|
||||||
const selectedTournamentDescription = document.getElementById('selected-tournament-description');
|
const selectedTournamentDescription = document.getElementById('selected-tournament-description');
|
||||||
const tournamentDisplay = document.getElementById('tournament-display');
|
const tournamentDisplay = document.getElementById('tournament-display');
|
||||||
const spectatorError = document.getElementById('spectator-error');
|
// ... (get elements for live matches, all matches, players, bracket, notifications as before)
|
||||||
|
|
||||||
const liveMatchesList = document.getElementById('live-matches-list');
|
const liveMatchesList = document.getElementById('live-matches-list');
|
||||||
const loadingLiveMatches = document.getElementById('loading-live-matches');
|
const loadingLiveMatches = document.getElementById('loading-live-matches');
|
||||||
const liveMatchesError = document.getElementById('live-matches-error');
|
const liveMatchesError = document.getElementById('live-matches-error');
|
||||||
|
|
||||||
const bracketViz = document.getElementById('bracket-groups-visualization');
|
const bracketViz = document.getElementById('bracket-groups-visualization');
|
||||||
const loadingBracket = document.getElementById('loading-bracket');
|
const loadingBracket = document.getElementById('loading-bracket');
|
||||||
const bracketError = document.getElementById('bracket-error');
|
const bracketError = document.getElementById('bracket-error');
|
||||||
|
|
||||||
const allMatchesList = document.getElementById('all-matches-list');
|
const allMatchesList = document.getElementById('all-matches-list');
|
||||||
const loadingAllMatches = document.getElementById('loading-all-matches');
|
const loadingAllMatches = document.getElementById('loading-all-matches');
|
||||||
const allMatchesError = document.getElementById('all-matches-error');
|
const allMatchesError = document.getElementById('all-matches-error');
|
||||||
|
|
||||||
const filterPlayerInput = document.getElementById('filter-player');
|
const filterPlayerInput = document.getElementById('filter-player');
|
||||||
const playerListDiv = document.getElementById('player-list');
|
const playerListDiv = document.getElementById('player-list');
|
||||||
const loadingPlayers = document.getElementById('loading-players');
|
const loadingPlayers = document.getElementById('loading-players');
|
||||||
const playerListError = document.getElementById('player-list-error');
|
const playerListError = document.getElementById('player-list-error');
|
||||||
|
|
||||||
const notificationsArea = document.getElementById('notifications-area');
|
const notificationsArea = document.getElementById('notifications-area');
|
||||||
const loadingNotifications = document.getElementById('loading-notifications');
|
const loadingNotifications = document.getElementById('loading-notifications');
|
||||||
const notificationsError = document.getElementById('notifications-error');
|
const notificationsError = document.getElementById('notifications-error');
|
||||||
|
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
let allPlayersCache = []; // Cache all players fetched once
|
let activeTournamentId = null;
|
||||||
let currentTournamentId = null;
|
let tournamentPlayersCacheSpec = []; // Cache players for the active tournament
|
||||||
|
|
||||||
// --- API Base URL ---
|
// --- API Base URL ---
|
||||||
const API_BASE_URL_SPEC = '/api';
|
const API_BASE_URL_SPEC = '/api';
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions (showMessageSpec, hideMessageSpec, setLoadingSpec) ---
|
||||||
const showMessageSpec = (element, message, isError = true) => {
|
const showMessageSpec = (element, message, isError = true) => { if (!element) return; element.textContent = message; element.className = `message-area ${isError ? 'error-message' : 'success-message'}`; element.classList.remove('hidden'); };
|
||||||
if (!element) return;
|
const hideMessageSpec = (element) => { if (element) { element.classList.add('hidden'); element.textContent = ''; } };
|
||||||
element.textContent = message;
|
const setLoadingSpec = (element, isLoading) => { if (element) { element.classList.toggle('hidden', !isLoading); } };
|
||||||
element.className = `message-area ${isError ? 'error-message' : 'success-message'}`;
|
|
||||||
element.classList.remove('hidden');
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideMessageSpec = (element) => {
|
|
||||||
if (element) {
|
|
||||||
element.classList.add('hidden');
|
|
||||||
element.textContent = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLoadingSpec = (element, isLoading) => {
|
|
||||||
if (element) {
|
|
||||||
element.classList.toggle('hidden', !isLoading);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- API Fetch Function (Spectator - No Auth Header) ---
|
// --- API Fetch Function (Spectator - No Auth Header) ---
|
||||||
// Spectator endpoints should ideally be public.
|
const fetchAPISpec = async (endpoint, options = {}) => { /* ... (same as spectator_html_v2) ... */
|
||||||
const fetchAPISpec = async (endpoint, options = {}) => {
|
|
||||||
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
||||||
// No Authorization header for public spectator view
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL_SPEC}${endpoint}`, { ...options, headers });
|
const response = await fetch(`${API_BASE_URL_SPEC}${endpoint}`, { ...options, headers });
|
||||||
if (!response.ok) {
|
if (!response.ok) { let e = { m: `HTTP-Fehler: ${response.status} ${response.statusText}` }; try { const p = await response.json(); if (p && p.message) e.m = p.message; } catch (err) {} throw new Error(e.m); }
|
||||||
let errorData = { message: `HTTP-Fehler: ${response.status} ${response.statusText}` };
|
if (response.status === 204) return null; return await response.json();
|
||||||
try {
|
} catch (error) { console.error('API Fetch Error (Spectator):', error); throw error; }
|
||||||
const parsedError = await response.json();
|
|
||||||
if (parsedError && parsedError.message) errorData.message = parsedError.message;
|
|
||||||
} catch (e) { /* Ignore */ }
|
|
||||||
throw new Error(errorData.message);
|
|
||||||
}
|
|
||||||
if (response.status === 204) return null;
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API Fetch Error (Spectator):', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Core Functions ---
|
// --- Core Functions ---
|
||||||
|
|
||||||
// Load available tournaments into the dropdown
|
// Find active tournament and load its data
|
||||||
const loadAvailableTournaments = async () => {
|
const loadActiveTournamentData = async () => {
|
||||||
setLoadingSpec(loadingTournamentsSpectator, true);
|
console.log("Suche aktives Turnier...");
|
||||||
selectTournament.disabled = true;
|
|
||||||
hideMessageSpec(spectatorError);
|
hideMessageSpec(spectatorError);
|
||||||
try {
|
noActiveTournamentMessageSpec.classList.add('hidden');
|
||||||
// Assuming /api/tournaments is accessible without auth
|
tournamentInfoHeader.classList.add('hidden');
|
||||||
const tournaments = await fetchAPISpec('/tournaments');
|
tournamentDisplay.classList.add('hidden');
|
||||||
|
activeTournamentId = null;
|
||||||
|
|
||||||
selectTournament.innerHTML = '<option value="">-- Bitte Turnier wählen --</option>'; // Reset
|
try {
|
||||||
if (tournaments.length === 0) {
|
const tournaments = await fetchAPISpec('/tournaments');
|
||||||
selectTournament.innerHTML += '<option value="" disabled>Keine Turniere verfügbar</option>';
|
const runningTournament = tournaments.find(t => t.status === 'running');
|
||||||
} else {
|
|
||||||
// Sort tournaments, e.g., by date descending
|
if (!runningTournament) {
|
||||||
tournaments.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
|
noActiveTournamentMessageSpec.classList.remove('hidden');
|
||||||
tournaments.forEach(t => {
|
console.log("Kein aktives Turnier gefunden.");
|
||||||
const option = document.createElement('option');
|
return; // Stop if no tournament is running
|
||||||
option.value = t.tournament_id;
|
|
||||||
option.textContent = `${t.name} (${t.date ? new Date(t.date).toLocaleDateString('de-DE') : 'Datum n.a.'})`;
|
|
||||||
selectTournament.appendChild(option);
|
|
||||||
});
|
|
||||||
selectTournament.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeTournamentId = runningTournament.tournament_id;
|
||||||
|
console.log(`Aktives Turnier gefunden: ${runningTournament.name} (${activeTournamentId})`);
|
||||||
|
|
||||||
|
// Display basic info and load dependent data
|
||||||
|
displayTournamentHeader(runningTournament);
|
||||||
|
await loadTournamentDependentData(activeTournamentId);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessageSpec(spectatorError, `Fehler beim Laden der Turniere: ${error.message}`);
|
showMessageSpec(spectatorError, `Fehler beim Laden des Turniers: ${error.message}`);
|
||||||
selectTournament.innerHTML += '<option value="" disabled>Fehler beim Laden</option>';
|
|
||||||
} finally {
|
|
||||||
setLoadingSpec(loadingTournamentsSpectator, false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load details and related data for the selected tournament
|
// Display header info for the loaded tournament
|
||||||
const loadTournamentDetails = async (tournamentId) => {
|
const displayTournamentHeader = (tournament) => {
|
||||||
console.log("Lade Details für Turnier:", tournamentId);
|
if (!tournament) return;
|
||||||
currentTournamentId = tournamentId; // Store current ID
|
selectedTournamentName.textContent = tournament.name || 'Unbekanntes Turnier';
|
||||||
hideMessageSpec(spectatorError);
|
const dateStr = tournament.date ? new Date(tournament.date).toLocaleDateString('de-DE') : 'N/A';
|
||||||
tournamentInfo.classList.add('hidden');
|
const typeStr = tournament.tournament_type === 'knockout' ? 'KO-System' : 'Gruppenphase';
|
||||||
tournamentDisplay.classList.add('hidden'); // Hide sections until loaded
|
selectedTournamentDetails.textContent = `Ort: ${tournament.location || 'N/A'} | Datum: ${dateStr} | Typ: ${typeStr}`;
|
||||||
|
selectedTournamentDescription.textContent = tournament.description || '';
|
||||||
setLoadingSpec(loadingLiveMatches, true);
|
tournamentInfoHeader.classList.remove('hidden');
|
||||||
setLoadingSpec(loadingBracket, true); // Show loading even if not implemented
|
|
||||||
setLoadingSpec(loadingAllMatches, true);
|
|
||||||
setLoadingSpec(loadingNotifications, true); // Show loading even if not implemented
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Fetch basic tournament info
|
|
||||||
const tournament = await fetchAPISpec(`/tournaments/${tournamentId}`);
|
|
||||||
selectedTournamentName.textContent = tournament.name || 'Unbekanntes Turnier';
|
|
||||||
const dateStr = tournament.date ? new Date(tournament.date).toLocaleDateString('de-DE') : 'N/A';
|
|
||||||
const typeStr = tournament.tournament_type === 'knockout' ? 'KO-System' : 'Gruppenphase';
|
|
||||||
selectedTournamentDetails.textContent = `Ort: ${tournament.location || 'N/A'} | Datum: ${dateStr} | Typ: ${typeStr}`;
|
|
||||||
selectedTournamentDescription.textContent = tournament.description || '';
|
|
||||||
tournamentInfo.classList.remove('hidden');
|
|
||||||
|
|
||||||
// 2. Trigger loading of related data concurrently
|
|
||||||
await Promise.all([
|
|
||||||
loadLiveMatches(tournamentId),
|
|
||||||
loadAllMatches(tournamentId),
|
|
||||||
loadBracketInfo(tournamentId), // Placeholder call
|
|
||||||
loadNotifications(tournamentId) // Placeholder call
|
|
||||||
]);
|
|
||||||
|
|
||||||
tournamentDisplay.classList.remove('hidden'); // Show the details sections
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
showMessageSpec(spectatorError, `Fehler beim Laden der Turnierdetails: ${error.message}`);
|
|
||||||
tournamentDisplay.classList.add('hidden'); // Hide details on error
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load and display live matches
|
// Load all data sections for the active tournament
|
||||||
const loadLiveMatches = async (tournamentId) => {
|
const loadTournamentDependentData = async (tournamentId) => {
|
||||||
setLoadingSpec(loadingLiveMatches, true);
|
if (!tournamentId) return;
|
||||||
liveMatchesList.innerHTML = '';
|
|
||||||
hideMessageSpec(liveMatchesError);
|
tournamentDisplay.classList.remove('hidden'); // Show the main display area
|
||||||
|
|
||||||
|
// Trigger loading concurrently
|
||||||
|
await Promise.all([
|
||||||
|
loadLiveMatches(tournamentId),
|
||||||
|
loadAllMatches(tournamentId),
|
||||||
|
loadTournamentPlayers(tournamentId), // Use new function
|
||||||
|
loadBracketInfo(tournamentId), // Placeholder
|
||||||
|
loadNotifications(tournamentId) // Placeholder
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Load and display live matches (same as spectator_html_v2)
|
||||||
|
const loadLiveMatches = async (tournamentId) => { /* ... (same as spectator_html_v2) ... */
|
||||||
|
setLoadingSpec(loadingLiveMatches, true); liveMatchesList.innerHTML = ''; hideMessageSpec(liveMatchesError);
|
||||||
try {
|
try {
|
||||||
const matches = await fetchAPISpec(`/matches?tournamentId=${tournamentId}&status=ongoing`);
|
const matches = await fetchAPISpec(`/matches?tournamentId=${tournamentId}&status=ongoing`);
|
||||||
if (matches.length === 0) {
|
if (matches.length === 0) { liveMatchesList.innerHTML = '<p><i>Aktuell keine laufenden Spiele.</i></p>'; }
|
||||||
liveMatchesList.innerHTML = '<p><i>Aktuell keine laufenden Spiele.</i></p>';
|
else {
|
||||||
} else {
|
|
||||||
matches.forEach(match => {
|
matches.forEach(match => {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div'); card.className = 'match-card';
|
||||||
card.className = 'match-card';
|
card.innerHTML = `<div class="players">${match.player1_name || 'N/A'} vs ${match.player2_name || 'N/A'}</div> <div class="details">Runde: ${match.round || '?'} | Tisch: ${match.table_number || '?'}</div>`;
|
||||||
// TODO: Display live score if available (requires backend changes)
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="players">${match.player1_name || 'N/A'} vs ${match.player2_name || 'N/A'}</div>
|
|
||||||
<div class="details">
|
|
||||||
Runde: ${match.round || '?'} | Tisch: ${match.table_number || '?'}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
liveMatchesList.appendChild(card);
|
liveMatchesList.appendChild(card);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) { showMessageSpec(liveMatchesError, `Fehler Live-Spiele: ${error.message}`); liveMatchesList.innerHTML = '<p><i>Fehler beim Laden.</i></p>'; }
|
||||||
showMessageSpec(liveMatchesError, `Fehler beim Laden der Live-Spiele: ${error.message}`);
|
finally { setLoadingSpec(loadingLiveMatches, false); }
|
||||||
liveMatchesList.innerHTML = '<p><i>Fehler beim Laden der Live-Spiele.</i></p>';
|
|
||||||
} finally {
|
|
||||||
setLoadingSpec(loadingLiveMatches, false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load and display all matches (schedule)
|
// Load and display all matches (schedule) (same as spectator_html_v2)
|
||||||
const loadAllMatches = async (tournamentId) => {
|
const loadAllMatches = async (tournamentId) => { /* ... (same as spectator_html_v2) ... */
|
||||||
setLoadingSpec(loadingAllMatches, true);
|
setLoadingSpec(loadingAllMatches, true); allMatchesList.innerHTML = ''; hideMessageSpec(allMatchesError);
|
||||||
allMatchesList.innerHTML = '';
|
|
||||||
hideMessageSpec(allMatchesError);
|
|
||||||
try {
|
try {
|
||||||
const matches = await fetchAPISpec(`/matches?tournamentId=${tournamentId}`);
|
const matches = await fetchAPISpec(`/matches?tournamentId=${tournamentId}`);
|
||||||
// Sort matches: by round, then match number, then scheduled time
|
matches.sort((a, b) => { /* Sorting logic */
|
||||||
matches.sort((a, b) => {
|
if ((a.round || 999) < (b.round || 999)) return -1; if ((a.round || 999) > (b.round || 999)) return 1;
|
||||||
if ((a.round || 999) < (b.round || 999)) return -1;
|
if ((a.match_number_in_round || 999) < (b.match_number_in_round || 999)) return -1; if ((a.match_number_in_round || 999) > (b.match_number_in_round || 999)) return 1;
|
||||||
if ((a.round || 999) > (b.round || 999)) return 1;
|
const timeA = a.scheduled_time ? new Date(a.scheduled_time).getTime() : Infinity; const timeB = b.scheduled_time ? new Date(b.scheduled_time).getTime() : Infinity; return timeA - timeB;
|
||||||
if ((a.match_number_in_round || 999) < (b.match_number_in_round || 999)) return -1;
|
|
||||||
if ((a.match_number_in_round || 999) > (b.match_number_in_round || 999)) return 1;
|
|
||||||
const timeA = a.scheduled_time ? new Date(a.scheduled_time).getTime() : Infinity;
|
|
||||||
const timeB = b.scheduled_time ? new Date(b.scheduled_time).getTime() : Infinity;
|
|
||||||
return timeA - timeB;
|
|
||||||
});
|
});
|
||||||
|
if (matches.length === 0) { allMatchesList.innerHTML = '<p><i>Keine Spiele für dieses Turnier gefunden.</i></p>'; }
|
||||||
|
else {
|
||||||
if (matches.length === 0) {
|
const table = document.createElement('table'); table.innerHTML = `<thead><tr><th>Runde</th><th>Spiel Nr.</th><th>Spieler 1</th><th>Spieler 2</th><th>Ergebnis</th><th>Status</th><th>Geplant</th><th>Tisch</th></tr></thead><tbody></tbody>`;
|
||||||
allMatchesList.innerHTML = '<p><i>Keine Spiele für dieses Turnier gefunden.</i></p>';
|
|
||||||
} else {
|
|
||||||
const table = document.createElement('table');
|
|
||||||
table.innerHTML = `
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Runde</th>
|
|
||||||
<th>Spiel Nr.</th>
|
|
||||||
<th>Spieler 1</th>
|
|
||||||
<th>Spieler 2</th>
|
|
||||||
<th>Ergebnis</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Geplant</th>
|
|
||||||
<th>Tisch</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
`;
|
|
||||||
const tbody = table.querySelector('tbody');
|
const tbody = table.querySelector('tbody');
|
||||||
matches.forEach(match => {
|
matches.forEach(match => {
|
||||||
const row = tbody.insertRow();
|
const row = tbody.insertRow(); const scheduled = match.scheduled_time ? new Date(match.scheduled_time).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short'}) : '-';
|
||||||
const scheduled = match.scheduled_time ? new Date(match.scheduled_time).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short'}) : '-';
|
let resultPlaceholder = '-'; if (match.status === 'finished') { resultPlaceholder = match.winner_name ? `${match.winner_name} gewinnt` : 'Beendet'; }
|
||||||
// TODO: Fetch and display actual scores/result string more accurately
|
row.innerHTML = `<td>${match.round || '-'}</td><td>${match.match_number_in_round || '-'}</td><td>${match.player1_name || '<i>N/A</i>'}</td><td>${match.player2_name || '<i>N/A</i>'}</td><td>${resultPlaceholder}</td><td>${match.status || '-'}</td><td>${scheduled}</td><td>${match.table_number || '-'}</td>`;
|
||||||
let resultPlaceholder = '-';
|
}); allMatchesList.appendChild(table);
|
||||||
if (match.status === 'finished') {
|
|
||||||
resultPlaceholder = match.winner_name ? `${match.winner_name} gewinnt` : 'Beendet';
|
|
||||||
// Ideally fetch sets: const sets = await fetchAPISpec(`/matches/${match.match_id}/sets`) ... display score string
|
|
||||||
}
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${match.round || '-'}</td>
|
|
||||||
<td>${match.match_number_in_round || '-'}</td>
|
|
||||||
<td>${match.player1_name || '<i>N/A</i>'}</td>
|
|
||||||
<td>${match.player2_name || '<i>N/A</i>'}</td>
|
|
||||||
<td>${resultPlaceholder}</td>
|
|
||||||
<td>${match.status || '-'}</td>
|
|
||||||
<td>${scheduled}</td>
|
|
||||||
<td>${match.table_number || '-'}</td>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
allMatchesList.appendChild(table);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) { showMessageSpec(allMatchesError, `Fehler Spielplan: ${error.message}`); allMatchesList.innerHTML = '<p><i>Fehler beim Laden.</i></p>'; }
|
||||||
|
finally { setLoadingSpec(loadingAllMatches, false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load and display players FOR THIS TOURNAMENT
|
||||||
|
const loadTournamentPlayers = async (tournamentId) => {
|
||||||
|
setLoadingSpec(loadingPlayers, true);
|
||||||
|
hideMessageSpec(playerListError);
|
||||||
|
tournamentPlayersCacheSpec = []; // Clear cache before loading new tournament's players
|
||||||
|
try {
|
||||||
|
// Use the new endpoint to get only players linked to this tournament
|
||||||
|
tournamentPlayersCacheSpec = await fetchAPISpec(`/tournaments/${tournamentId}/players`);
|
||||||
|
tournamentPlayersCacheSpec.sort((a,b) => (a.last_name || '').localeCompare(b.last_name || '')); // Sort by last name
|
||||||
|
filterAndRenderPlayersSpec(); // Initial render without filter
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessageSpec(allMatchesError, `Fehler beim Laden des Spielplans: ${error.message}`);
|
showMessageSpec(playerListError, `Fehler beim Laden der Teilnehmer: ${error.message}`);
|
||||||
allMatchesList.innerHTML = '<p><i>Fehler beim Laden des Spielplans.</i></p>';
|
tournamentPlayersCacheSpec = []; // Ensure cache is empty on error
|
||||||
|
filterAndRenderPlayersSpec(); // Render empty state
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSpec(loadingAllMatches, false);
|
setLoadingSpec(loadingPlayers, false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load and display players (currently ALL players)
|
// Filter and render the TOURNAMENT player list based on search term
|
||||||
const loadAndDisplayPlayers = async () => {
|
const filterAndRenderPlayersSpec = () => {
|
||||||
// Only fetch all players once if not already cached
|
|
||||||
if (allPlayersCache.length === 0) {
|
|
||||||
setLoadingSpec(loadingPlayers, true);
|
|
||||||
hideMessageSpec(playerListError);
|
|
||||||
try {
|
|
||||||
// Fetch ALL players - backend endpoint for tournament-specific players is needed
|
|
||||||
console.warn("Fetching ALL players for spectator view. Backend should provide tournament-specific player list.");
|
|
||||||
allPlayersCache = await fetchAPISpec('/players');
|
|
||||||
allPlayersCache.sort((a,b) => (a.last_name || '').localeCompare(b.last_name || '')); // Sort by last name
|
|
||||||
} catch (error) {
|
|
||||||
showMessageSpec(playerListError, `Fehler beim Laden der Spielerliste: ${error.message}`);
|
|
||||||
allPlayersCache = []; // Ensure cache is empty on error
|
|
||||||
} finally {
|
|
||||||
setLoadingSpec(loadingPlayers, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Filter and display based on the current cache and search term
|
|
||||||
filterAndRenderPlayers();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter and render the player list based on search term
|
|
||||||
const filterAndRenderPlayers = () => {
|
|
||||||
const searchTerm = filterPlayerInput.value.trim().toLowerCase();
|
const searchTerm = filterPlayerInput.value.trim().toLowerCase();
|
||||||
const filteredPlayers = allPlayersCache.filter(p => {
|
const filteredPlayers = tournamentPlayersCacheSpec.filter(p => {
|
||||||
return !searchTerm ||
|
return !searchTerm ||
|
||||||
(p.first_name && p.first_name.toLowerCase().includes(searchTerm)) ||
|
(p.first_name && p.first_name.toLowerCase().includes(searchTerm)) ||
|
||||||
(p.last_name && p.last_name.toLowerCase().includes(searchTerm)) ||
|
(p.last_name && p.last_name.toLowerCase().includes(searchTerm)) ||
|
||||||
@@ -406,7 +265,7 @@
|
|||||||
|
|
||||||
playerListDiv.innerHTML = ''; // Clear previous list
|
playerListDiv.innerHTML = ''; // Clear previous list
|
||||||
if (filteredPlayers.length === 0) {
|
if (filteredPlayers.length === 0) {
|
||||||
playerListDiv.innerHTML = '<p><i>Keine Spieler entsprechen dem Suchbegriff.</i></p>';
|
playerListDiv.innerHTML = searchTerm ? '<p><i>Keine Teilnehmer entsprechen dem Suchbegriff.</i></p>' : '<p><i>Keine Teilnehmer für dieses Turnier gefunden.</i></p>';
|
||||||
} else {
|
} else {
|
||||||
const table = document.createElement('table');
|
const table = document.createElement('table');
|
||||||
table.innerHTML = `
|
table.innerHTML = `
|
||||||
@@ -414,79 +273,55 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Nachname</th>
|
<th>Nachname</th>
|
||||||
<th>Vorname</th>
|
<th>Vorname</th>
|
||||||
|
<th>Geschlecht</th> {/* Added Header */}
|
||||||
<th>Verein</th>
|
<th>Verein</th>
|
||||||
<th>QTTR</th>
|
<th>QTTR</th>
|
||||||
<th>Altersklasse</th>
|
<th>Altersklasse</th> {/* Added Header - assuming it comes from players table */}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>`;
|
<tbody></tbody>`;
|
||||||
const tbody = table.querySelector('tbody');
|
const tbody = table.querySelector('tbody');
|
||||||
filteredPlayers.forEach(player => {
|
filteredPlayers.forEach(player => {
|
||||||
const row = tbody.insertRow();
|
const row = tbody.insertRow();
|
||||||
|
let genderText = '-';
|
||||||
|
if (player.gender === 'm') genderText = 'M';
|
||||||
|
else if (player.gender === 'w') genderText = 'W';
|
||||||
|
else if (player.gender === 'd') genderText = 'D';
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${player.last_name || '-'}</td>
|
<td>${player.last_name || '-'}</td>
|
||||||
<td>${player.first_name || '-'}</td>
|
<td>${player.first_name || '-'}</td>
|
||||||
|
<td>${genderText}</td> {/* Display Gender */}
|
||||||
<td>${player.club || '-'}</td>
|
<td>${player.club || '-'}</td>
|
||||||
<td>${player.qttr_points || '-'}</td>
|
<td>${player.qttr_points || '-'}</td>
|
||||||
<td>${player.age_class || '-'}</td>
|
<td>${player.age_class || '-'}</td> {/* Display Age Class */}
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
playerListDiv.appendChild(table);
|
playerListDiv.appendChild(table);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Placeholder for loading bracket info
|
// Placeholder for loading bracket info (same as spectator_html_v2)
|
||||||
const loadBracketInfo = async (tournamentId) => {
|
const loadBracketInfo = async (tournamentId) => { /* ... (placeholder) ... */ setLoadingSpec(loadingBracket, true); bracketViz.innerHTML = '<p><i>...</i></p>'; hideMessageSpec(bracketError); await new Promise(r => setTimeout(r, 100)); setLoadingSpec(loadingBracket, false); bracketViz.innerHTML = '<p><i>Visualisierung noch nicht implementiert.</i></p>'; };
|
||||||
setLoadingSpec(loadingBracket, true);
|
|
||||||
bracketViz.innerHTML = '<p><i>Turnierbaum-Visualisierung wird geladen... (Funktion noch nicht implementiert)</i></p>';
|
|
||||||
hideMessageSpec(bracketError);
|
|
||||||
// TODO: Implement API call to fetch bracket data
|
|
||||||
// Example: const bracketData = await fetchAPISpec(`/tournaments/${tournamentId}/bracket`);
|
|
||||||
// TODO: Use a library (e.g., jquery-bracket, d3) or custom rendering to display the bracket
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate loading
|
|
||||||
setLoadingSpec(loadingBracket, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Placeholder for loading notifications
|
// Placeholder for loading notifications (same as spectator_html_v2)
|
||||||
const loadNotifications = async (tournamentId) => {
|
const loadNotifications = async (tournamentId) => { /* ... (placeholder) ... */ setLoadingSpec(loadingNotifications, true); notificationsArea.innerHTML = '<p><i>...</i></p>'; hideMessageSpec(notificationsError); await new Promise(r => setTimeout(r, 100)); setLoadingSpec(loadingNotifications, false); notificationsArea.innerHTML = '<p><i>Keine Hinweise oder Funktion nicht implementiert.</i></p>';};
|
||||||
setLoadingSpec(loadingNotifications, true);
|
|
||||||
notificationsArea.innerHTML = '<p><i>Hinweise werden geladen... (Funktion noch nicht implementiert)</i></p>';
|
|
||||||
hideMessageSpec(notificationsError);
|
|
||||||
// TODO: Implement API call to fetch notifications
|
|
||||||
// Example: const notes = await fetchAPISpec(`/notifications?tournamentId=${tournamentId}&target=spectators`);
|
|
||||||
// TODO: Display notifications
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate loading
|
|
||||||
setLoadingSpec(loadingNotifications, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// --- Event Listeners ---
|
// --- Event Listeners ---
|
||||||
selectTournament.addEventListener('change', (event) => {
|
|
||||||
const selectedId = event.target.value;
|
|
||||||
if (selectedId) {
|
|
||||||
loadTournamentDetails(selectedId);
|
|
||||||
} else {
|
|
||||||
currentTournamentId = null;
|
|
||||||
tournamentInfo.classList.add('hidden');
|
|
||||||
tournamentDisplay.classList.add('hidden');
|
|
||||||
hideMessageSpec(spectatorError);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Player filter input (debounced)
|
// Player filter input (debounced)
|
||||||
let playerFilterTimeout;
|
let playerFilterTimeoutSpec;
|
||||||
filterPlayerInput.addEventListener('input', () => {
|
filterPlayerInput.addEventListener('input', () => {
|
||||||
clearTimeout(playerFilterTimeout);
|
clearTimeout(playerFilterTimeoutSpec);
|
||||||
playerFilterTimeout = setTimeout(() => {
|
playerFilterTimeoutSpec = setTimeout(() => {
|
||||||
filterAndRenderPlayers(); // Filter based on cached players
|
filterAndRenderPlayersSpec(); // Filter based on cached tournament players
|
||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Initial Load ---
|
// --- Initial Load ---
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadAvailableTournaments();
|
loadActiveTournamentData(); // Automatically load data for the running tournament
|
||||||
loadAndDisplayPlayers(); // Load global player list initially
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@@ -25,6 +25,7 @@ const userRoutes = require('./src/routes/userRoutes');
|
|||||||
|
|
||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const HTTPS_PORT = process.env.HTTPS_PORT || 3443; // Optional: For HTTPS port if needed
|
||||||
|
|
||||||
// --- Initialize Express App ---
|
// --- Initialize Express App ---
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -101,9 +102,13 @@ const startServer = async () => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Start HTTPS server if certificates are loaded
|
// Start HTTPS server if certificates are loaded
|
||||||
|
// if (httpsOptions.key && httpsOptions.cert) {
|
||||||
|
// https.createServer(httpsOptions, app).listen(PORT, () => {
|
||||||
|
// console.log(`Server running in HTTPS mode on https://localhost:${PORT}`);
|
||||||
|
// });
|
||||||
if (httpsOptions.key && httpsOptions.cert) {
|
if (httpsOptions.key && httpsOptions.cert) {
|
||||||
https.createServer(httpsOptions, app).listen(PORT, () => {
|
https.createServer(httpsOptions, app).listen(HTTPS_PORT, () => {
|
||||||
console.log(`Server running in HTTPS mode on https://localhost:${PORT}`);
|
console.log(`HTTPS server running on https://localhost:${HTTPS_PORT}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Fallback to HTTP if certificates are missing
|
// Fallback to HTTP if certificates are missing
|
||||||
|
@@ -4,7 +4,7 @@ const jwt = require('jsonwebtoken');
|
|||||||
const db = require('../db/db'); // Import database query function
|
const db = require('../db/db'); // Import database query function
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET;
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
const SALT_ROUNDS = 10; // Cost factor for bcrypt hashing
|
const SALT_ROUNDS = 12; // Cost factor for bcrypt hashing
|
||||||
|
|
||||||
// Register a new user
|
// Register a new user
|
||||||
exports.register = async (req, res, next) => {
|
exports.register = async (req, res, next) => {
|
||||||
@@ -16,6 +16,18 @@ exports.register = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add more robust validation (e.g., password complexity, username format)
|
// TODO: Add more robust validation (e.g., password complexity, username format)
|
||||||
|
const validatePassword = (password) => {
|
||||||
|
const minLength = 8;
|
||||||
|
const hasUpperCase = /[A-Z]/.test(password);
|
||||||
|
const hasLowerCase = /[a-z]/.test(password);
|
||||||
|
const hasNumbers = /\d/.test(password);
|
||||||
|
const hasSpecialChars = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
||||||
|
|
||||||
|
return password.length >= minLength && hasUpperCase &&
|
||||||
|
hasLowerCase && hasNumbers && hasSpecialChars;
|
||||||
|
};
|
||||||
|
|
||||||
|
// validatePassword(password) ? null : res.status(400).json({ message: 'Passwort muss mindestens 8 Zeichen lang sein und Großbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten.' });
|
||||||
|
|
||||||
// Determine the role - default to 'spectator' if not provided or invalid
|
// Determine the role - default to 'spectator' if not provided or invalid
|
||||||
// In a real scenario, only admins should be able to assign 'admin' or 'referee' roles.
|
// In a real scenario, only admins should be able to assign 'admin' or 'referee' roles.
|
||||||
|
129
src/controllers/errorController.js
Normal file
129
src/controllers/errorController.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Error controller to standardize error handling across the application
|
||||||
|
* Works alongside errorMiddleware.js but provides more granular control
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Predefined error responses
|
||||||
|
const ErrorTypes = {
|
||||||
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
||||||
|
DATABASE_ERROR: 'DATABASE_ERROR',
|
||||||
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
|
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||||
|
FORBIDDEN: 'FORBIDDEN',
|
||||||
|
CONFLICT: 'CONFLICT',
|
||||||
|
INTERNAL_ERROR: 'INTERNAL_ERROR'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a standard error response
|
||||||
|
* @param {string} type - Error type from ErrorTypes
|
||||||
|
* @param {string} message - Error message
|
||||||
|
* @param {object} details - Additional error details (optional)
|
||||||
|
* @param {number} status - HTTP status code (derived from type if not provided)
|
||||||
|
* @returns {Error} Custom error object with additional properties
|
||||||
|
*/
|
||||||
|
const createError = (type, message, details = null, status = null) => {
|
||||||
|
const error = new Error(message);
|
||||||
|
error.type = type;
|
||||||
|
error.details = details;
|
||||||
|
|
||||||
|
// Set appropriate status code based on error type
|
||||||
|
switch (type) {
|
||||||
|
case ErrorTypes.VALIDATION_ERROR:
|
||||||
|
error.statusCode = 400;
|
||||||
|
break;
|
||||||
|
case ErrorTypes.UNAUTHORIZED:
|
||||||
|
error.statusCode = 401;
|
||||||
|
break;
|
||||||
|
case ErrorTypes.FORBIDDEN:
|
||||||
|
error.statusCode = 403;
|
||||||
|
break;
|
||||||
|
case ErrorTypes.NOT_FOUND:
|
||||||
|
error.statusCode = 404;
|
||||||
|
break;
|
||||||
|
case ErrorTypes.CONFLICT:
|
||||||
|
error.statusCode = 409;
|
||||||
|
break;
|
||||||
|
case ErrorTypes.DATABASE_ERROR:
|
||||||
|
error.statusCode = 500;
|
||||||
|
break;
|
||||||
|
case ErrorTypes.INTERNAL_ERROR:
|
||||||
|
error.statusCode = 500;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error.statusCode = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override with custom status if provided
|
||||||
|
if (status) {
|
||||||
|
error.statusCode = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Specific error creation helpers
|
||||||
|
const validationError = (message, details = null) =>
|
||||||
|
createError(ErrorTypes.VALIDATION_ERROR, message, details);
|
||||||
|
|
||||||
|
const notFoundError = (resource, id = null) => {
|
||||||
|
const message = id
|
||||||
|
? `${resource} mit ID ${id} nicht gefunden.`
|
||||||
|
: `${resource} nicht gefunden.`;
|
||||||
|
return createError(ErrorTypes.NOT_FOUND, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const conflictError = (message, details = null) =>
|
||||||
|
createError(ErrorTypes.CONFLICT, message, details);
|
||||||
|
|
||||||
|
const databaseError = (error) => {
|
||||||
|
let message = 'Datenbankfehler.';
|
||||||
|
let details = null;
|
||||||
|
|
||||||
|
// Extract detail from PostgreSQL error if available
|
||||||
|
if (error.code) {
|
||||||
|
details = { pgCode: error.code, pgDetail: error.detail };
|
||||||
|
|
||||||
|
// Customize messages for common database errors
|
||||||
|
switch (error.code) {
|
||||||
|
case '23505': // unique_violation
|
||||||
|
message = 'Ein Eintrag mit diesen Daten existiert bereits.';
|
||||||
|
return createError(ErrorTypes.CONFLICT, message, details, 409);
|
||||||
|
case '23503': // foreign_key_violation
|
||||||
|
message = 'Der verknüpfte Eintrag existiert nicht oder kann nicht gelöscht werden.';
|
||||||
|
return createError(ErrorTypes.VALIDATION_ERROR, message, details, 400);
|
||||||
|
case '22P02': // invalid_text_representation (often for invalid UUIDs)
|
||||||
|
message = 'Ungültiges Datenformat.';
|
||||||
|
return createError(ErrorTypes.VALIDATION_ERROR, message, details, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createError(ErrorTypes.DATABASE_ERROR, message, details);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unauthorizedError = (message = 'Nicht autorisiert.') =>
|
||||||
|
createError(ErrorTypes.UNAUTHORIZED, message);
|
||||||
|
|
||||||
|
const forbiddenError = (message = 'Zugriff verweigert.') =>
|
||||||
|
createError(ErrorTypes.FORBIDDEN, message);
|
||||||
|
|
||||||
|
const internalError = (error) => {
|
||||||
|
console.error('Internal Server Error:', error);
|
||||||
|
return createError(
|
||||||
|
ErrorTypes.INTERNAL_ERROR,
|
||||||
|
'Ein interner Serverfehler ist aufgetreten.',
|
||||||
|
process.env.NODE_ENV === 'development' ? { stack: error.stack } : null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ErrorTypes,
|
||||||
|
createError,
|
||||||
|
validationError,
|
||||||
|
notFoundError,
|
||||||
|
conflictError,
|
||||||
|
databaseError,
|
||||||
|
unauthorizedError,
|
||||||
|
forbiddenError,
|
||||||
|
internalError
|
||||||
|
};
|
@@ -1,15 +1,14 @@
|
|||||||
// src/controllers/playerController.js
|
// src/controllers/playerController.js
|
||||||
const db = require('../db/db');
|
const db = require('../db/db');
|
||||||
|
|
||||||
// Get all players - now includes search functionality
|
// Get all players - includes search and gender
|
||||||
exports.getAllPlayers = async (req, res, next) => {
|
exports.getAllPlayers = async (req, res, next) => {
|
||||||
const { search } = req.query;
|
const { search } = req.query;
|
||||||
let query = 'SELECT player_id, first_name, last_name, club, qttr_points, age_class FROM players';
|
// Select gender along with other fields
|
||||||
|
let query = 'SELECT player_id, first_name, last_name, gender, club, qttr_points, age_class FROM players';
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
// Search across multiple fields using ILIKE for case-insensitivity
|
|
||||||
// Concatenate names for searching full name
|
|
||||||
query += ` WHERE (first_name || ' ' || last_name) ILIKE $1
|
query += ` WHERE (first_name || ' ' || last_name) ILIKE $1
|
||||||
OR (last_name || ' ' || first_name) ILIKE $1
|
OR (last_name || ' ' || first_name) ILIKE $1
|
||||||
OR club ILIKE $1
|
OR club ILIKE $1
|
||||||
@@ -17,7 +16,7 @@ exports.getAllPlayers = async (req, res, next) => {
|
|||||||
params.push(`%${search}%`);
|
params.push(`%${search}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
query += ' ORDER BY last_name, first_name'; // Default sorting
|
query += ' ORDER BY last_name, first_name';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db.query(query, params);
|
const result = await db.query(query, params);
|
||||||
@@ -27,14 +26,13 @@ exports.getAllPlayers = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get a single player by ID
|
// Get a single player by ID - includes gender
|
||||||
exports.getPlayerById = async (req, res, next) => {
|
exports.getPlayerById = async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
if (!id || id.toLowerCase() === 'export' || id.toLowerCase() === 'import') { // Avoid conflict with potential routes
|
if (!id || id.toLowerCase() === 'export' || id.toLowerCase() === 'import') { return next(); }
|
||||||
return next();
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const result = await db.query('SELECT player_id, first_name, last_name, club, qttr_points, age_class, created_at, updated_at FROM players WHERE player_id = $1', [id]);
|
// Select gender
|
||||||
|
const result = await db.query('SELECT player_id, first_name, last_name, gender, club, qttr_points, age_class, created_at, updated_at FROM players WHERE player_id = $1', [id]);
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
return res.status(404).json({ message: 'Spieler nicht gefunden.' });
|
return res.status(404).json({ message: 'Spieler nicht gefunden.' });
|
||||||
}
|
}
|
||||||
@@ -44,39 +42,47 @@ exports.getPlayerById = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a new player (Admin only - protected in router)
|
// Create a new player (Admin only) - includes gender
|
||||||
exports.createPlayer = async (req, res, next) => {
|
exports.createPlayer = async (req, res, next) => {
|
||||||
const { first_name, last_name, club, qttr_points, age_class } = req.body;
|
const { first_name, last_name, gender, club, qttr_points, age_class } = req.body; // Added gender
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if (!first_name || !last_name) {
|
if (!first_name || !last_name) {
|
||||||
return res.status(400).json({ message: 'Vorname und Nachname sind erforderlich.' });
|
return res.status(400).json({ message: 'Vorname und Nachname sind erforderlich.' });
|
||||||
}
|
}
|
||||||
|
// Validate gender
|
||||||
|
const validGenders = ['m', 'w', 'd'];
|
||||||
|
if (!gender || !validGenders.includes(gender)) {
|
||||||
|
return res.status(400).json({ message: 'Gültiges Geschlecht (m, w, d) ist erforderlich.' });
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure qttr_points is a number or null
|
|
||||||
const qtt_numeric = qttr_points ? parseInt(qttr_points, 10) : null;
|
const qtt_numeric = qttr_points ? parseInt(qttr_points, 10) : null;
|
||||||
if (qttr_points && (isNaN(qtt_numeric) || qtt_numeric < 0)) { // Allow 0 points, but not negative
|
if (qttr_points && (isNaN(qtt_numeric) || qtt_numeric < 0)) {
|
||||||
return res.status(400).json({ message: 'QTTR-Punkte müssen eine positive Zahl oder 0 sein.' });
|
return res.status(400).json({ message: 'QTTR-Punkte müssen eine positive Zahl oder 0 sein.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db.query(
|
const result = await db.query(
|
||||||
`INSERT INTO players (first_name, last_name, club, qttr_points, age_class)
|
// Include gender in INSERT and RETURNING
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
`INSERT INTO players (first_name, last_name, gender, club, qttr_points, age_class)
|
||||||
RETURNING player_id, first_name, last_name, club, qttr_points, age_class`,
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
[first_name, last_name, club || null, qtt_numeric, age_class || null]
|
RETURNING player_id, first_name, last_name, gender, club, qttr_points, age_class`,
|
||||||
|
[first_name, last_name, gender, club || null, qtt_numeric, age_class || null]
|
||||||
);
|
);
|
||||||
res.status(201).json(result.rows[0]);
|
res.status(201).json(result.rows[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Handle potential enum constraint violation for gender
|
||||||
|
if (error.code === '23514' && error.constraint?.includes('gender')) { // Check error code and constraint name
|
||||||
|
return res.status(400).json({ message: `Ungültiger Wert für Geschlecht: ${gender}. Erlaubt sind 'm', 'w', 'd'.` });
|
||||||
|
}
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update an existing player (Admin only - protected in router)
|
// Update an existing player (Admin only) - includes gender
|
||||||
exports.updatePlayer = async (req, res, next) => {
|
exports.updatePlayer = async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { first_name, last_name, club, qttr_points, age_class } = req.body;
|
const { first_name, last_name, gender, club, qttr_points, age_class } = req.body; // Added gender
|
||||||
|
|
||||||
// Build query dynamically
|
// Build query dynamically
|
||||||
const fieldsToUpdate = [];
|
const fieldsToUpdate = [];
|
||||||
@@ -84,16 +90,21 @@ exports.updatePlayer = async (req, res, next) => {
|
|||||||
let queryIndex = 1;
|
let queryIndex = 1;
|
||||||
|
|
||||||
const addUpdate = (field, value) => {
|
const addUpdate = (field, value) => {
|
||||||
// Allow explicitly setting fields to null or empty string (which becomes null for text fields)
|
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
let processedValue = value;
|
let processedValue = value;
|
||||||
if (field === 'qttr_points') {
|
if (field === 'qttr_points') {
|
||||||
processedValue = value ? parseInt(value, 10) : null;
|
processedValue = value !== null && value !== '' ? parseInt(value, 10) : null; // Allow setting to null
|
||||||
if (value && (isNaN(processedValue) || processedValue < 0)) {
|
if (value !== null && value !== '' && (isNaN(processedValue) || processedValue < 0)) {
|
||||||
throw new Error('QTTR-Punkte müssen eine positive Zahl oder 0 sein.');
|
throw new Error('QTTR-Punkte müssen eine positive Zahl oder 0 sein.');
|
||||||
}
|
}
|
||||||
} else {
|
} else if (field === 'gender') {
|
||||||
// Treat empty strings as null for nullable text fields like club, age_class
|
if (value && !['m', 'w', 'd'].includes(value)) {
|
||||||
|
throw new Error(`Ungültiges Geschlecht: ${value}. Erlaubt sind 'm', 'w', 'd'.`);
|
||||||
|
}
|
||||||
|
// Allow setting gender to null if needed? DB schema allows null currently.
|
||||||
|
processedValue = value || null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
processedValue = (value === '' && (field === 'club' || field === 'age_class')) ? null : value;
|
processedValue = (value === '' && (field === 'club' || field === 'age_class')) ? null : value;
|
||||||
}
|
}
|
||||||
fieldsToUpdate.push(`${field} = $${queryIndex++}`);
|
fieldsToUpdate.push(`${field} = $${queryIndex++}`);
|
||||||
@@ -102,12 +113,12 @@ exports.updatePlayer = async (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate required fields if they are being updated to empty
|
|
||||||
if (first_name === '') return res.status(400).json({ message: 'Vorname darf nicht leer sein.' });
|
if (first_name === '') return res.status(400).json({ message: 'Vorname darf nicht leer sein.' });
|
||||||
if (last_name === '') return res.status(400).json({ message: 'Nachname darf nicht leer sein.' });
|
if (last_name === '') return res.status(400).json({ message: 'Nachname darf nicht leer sein.' });
|
||||||
|
|
||||||
addUpdate('first_name', first_name);
|
addUpdate('first_name', first_name);
|
||||||
addUpdate('last_name', last_name);
|
addUpdate('last_name', last_name);
|
||||||
|
addUpdate('gender', gender); // Add gender update
|
||||||
addUpdate('club', club);
|
addUpdate('club', club);
|
||||||
addUpdate('qttr_points', qttr_points);
|
addUpdate('qttr_points', qttr_points);
|
||||||
addUpdate('age_class', age_class);
|
addUpdate('age_class', age_class);
|
||||||
@@ -116,9 +127,10 @@ exports.updatePlayer = async (req, res, next) => {
|
|||||||
return res.status(400).json({ message: 'Keine Daten zum Aktualisieren angegeben.' });
|
return res.status(400).json({ message: 'Keine Daten zum Aktualisieren angegeben.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
values.push(id); // Add player_id for WHERE clause
|
values.push(id);
|
||||||
|
|
||||||
const updateQuery = `UPDATE players SET ${fieldsToUpdate.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE player_id = $${queryIndex} RETURNING player_id, first_name, last_name, club, qttr_points, age_class`;
|
// Include gender in RETURNING
|
||||||
|
const updateQuery = `UPDATE players SET ${fieldsToUpdate.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE player_id = $${queryIndex} RETURNING player_id, first_name, last_name, gender, club, qttr_points, age_class`;
|
||||||
|
|
||||||
const result = await db.query(updateQuery, values);
|
const result = await db.query(updateQuery, values);
|
||||||
|
|
||||||
@@ -128,63 +140,46 @@ exports.updatePlayer = async (req, res, next) => {
|
|||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle potential validation errors from addUpdate
|
if (error.message.includes('QTTR-Punkte') || error.message.includes('Geschlecht')) {
|
||||||
if (error.message.includes('QTTR-Punkte')) {
|
|
||||||
return res.status(400).json({ message: error.message });
|
return res.status(400).json({ message: error.message });
|
||||||
}
|
}
|
||||||
|
// Handle potential enum constraint violation for gender on update
|
||||||
|
if (error.code === '23514' && error.constraint?.includes('gender')) {
|
||||||
|
return res.status(400).json({ message: `Ungültiger Wert für Geschlecht. Erlaubt sind 'm', 'w', 'd'.` });
|
||||||
|
}
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete a player (Admin only - protected in router)
|
// Delete a player (Admin only) - no changes needed for gender
|
||||||
exports.deletePlayer = async (req, res, next) => {
|
exports.deletePlayer = async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
try {
|
try {
|
||||||
// Check if player is linked in tournament_players (more restrictive than just matches)
|
|
||||||
const checkResult = await db.query('SELECT 1 FROM tournament_players WHERE player_id = $1 LIMIT 1', [id]);
|
const checkResult = await db.query('SELECT 1 FROM tournament_players WHERE player_id = $1 LIMIT 1', [id]);
|
||||||
if (checkResult.rows.length > 0) {
|
if (checkResult.rows.length > 0) {
|
||||||
return res.status(409).json({ message: 'Spieler kann nicht gelöscht werden, da er einem oder mehreren Turnieren zugeordnet ist.' });
|
return res.status(409).json({ message: 'Spieler kann nicht gelöscht werden, da er einem oder mehreren Turnieren zugeordnet ist.' });
|
||||||
}
|
}
|
||||||
// Check if player is linked in matches (as player1, player2, or winner) where status is not 'finished' or 'cancelled'?
|
|
||||||
// This might be too complex or restrictive. Relying on tournament_players check might be sufficient.
|
|
||||||
// ON DELETE SET NULL in matches table handles completed matches okay.
|
|
||||||
|
|
||||||
const result = await db.query('DELETE FROM players WHERE player_id = $1 RETURNING player_id', [id]);
|
const result = await db.query('DELETE FROM players WHERE player_id = $1 RETURNING player_id', [id]);
|
||||||
if (result.rowCount === 0) {
|
if (result.rowCount === 0) {
|
||||||
return res.status(404).json({ message: 'Spieler nicht gefunden.' });
|
return res.status(404).json({ message: 'Spieler nicht gefunden.' });
|
||||||
}
|
}
|
||||||
res.status(204).send(); // No Content
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle potential foreign key issues if not handled by CASCADE/SET NULL
|
|
||||||
if (error.code === '23503') {
|
if (error.code === '23503') {
|
||||||
console.warn(`Attempted to delete player ${id} which might still be referenced elsewhere unexpectedly.`);
|
console.warn(`Delete player ${id} FK violation: ${error.detail}`);
|
||||||
return res.status(409).json({ message: 'Spieler konnte nicht gelöscht werden, da er noch in Verwendung ist. Überprüfen Sie die Datenbank-Constraints.' });
|
return res.status(409).json({ message: 'Spieler konnte nicht gelöscht werden (Referenzfehler).' });
|
||||||
}
|
}
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Placeholder functions for Import/Export ---
|
// --- Placeholder functions for Import/Export --- (no changes needed for gender here yet)
|
||||||
|
exports.importPlayers = async (req, res, next) => { /* ... placeholder ... */ res.status(501).json({ message: 'Funktion Spieler-Import noch nicht implementiert.' }); };
|
||||||
exports.importPlayers = async (req, res, next) => {
|
exports.exportPlayers = async (req, res, next) => { /* ... placeholder ... */
|
||||||
// TODO: Implement CSV import logic
|
|
||||||
console.log("Received file for import (implement logic):", req.file); // Example if using multer
|
|
||||||
res.status(501).json({ message: 'Funktion Spieler-Import noch nicht implementiert.' });
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.exportPlayers = async (req, res, next) => {
|
|
||||||
// TODO: Implement CSV export logic using fast-csv
|
|
||||||
try {
|
try {
|
||||||
const players = await db.query('SELECT first_name, last_name, club, qttr_points, age_class FROM players ORDER BY last_name');
|
const players = await db.query('SELECT first_name, last_name, gender, club, qttr_points, age_class FROM players ORDER BY last_name'); // Added gender
|
||||||
if (players.rows.length === 0) {
|
if (players.rows.length === 0) { return res.status(404).json({ message: "Keine Spieler zum Exportieren gefunden." }); }
|
||||||
return res.status(404).json({ message: "Keine Spieler zum Exportieren gefunden." });
|
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Disposition', 'attachment; filename="spieler_export.json"'); res.json(players.rows);
|
||||||
}
|
} catch (error) { next(error); }
|
||||||
// Placeholder: Send JSON
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.setHeader('Content-Disposition', 'attachment; filename="spieler_export.json"');
|
|
||||||
res.json(players.rows);
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@@ -1,192 +1,93 @@
|
|||||||
// src/controllers/tournamentController.js
|
// src/controllers/tournamentController.js
|
||||||
const db = require('../db/db');
|
const db = require('../db/db');
|
||||||
|
|
||||||
|
// --- Bestehende Funktionen (getAllTournaments, getTournamentById, createTournament, updateTournament, deleteTournament) ---
|
||||||
|
// ... (Code von tt_tournament_tournament_controller_v3 bleibt hier unverändert) ...
|
||||||
// Get all tournaments
|
// Get all tournaments
|
||||||
exports.getAllTournaments = async (req, res, next) => {
|
exports.getAllTournaments = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
// Add filtering or pagination later if needed
|
const result = await db.query('SELECT * FROM tournaments ORDER BY date DESC NULLS LAST, created_at DESC');
|
||||||
const result = await db.query('SELECT * FROM tournaments ORDER BY date DESC NULLS LAST, created_at DESC'); // Handle null dates
|
|
||||||
res.json(result.rows);
|
res.json(result.rows);
|
||||||
} catch (error) {
|
} catch (error) { next(error); }
|
||||||
next(error); // Pass error to the error handler
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get a single tournament by ID
|
// Get a single tournament by ID
|
||||||
exports.getTournamentById = async (req, res, next) => {
|
exports.getTournamentById = async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
if (!id || id.toLowerCase() === 'export') { // Avoid conflict with potential export route if not handled carefully
|
// Updated check to avoid conflict with '/export' or '/:id/players'
|
||||||
return next(); // Skip if ID is 'export' or similar keyword
|
if (!id || ['export', 'import'].includes(id.toLowerCase()) || req.path.includes('/players')) {
|
||||||
|
return next();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await db.query('SELECT * FROM tournaments WHERE tournament_id = $1', [id]);
|
const result = await db.query('SELECT * FROM tournaments WHERE tournament_id = $1', [id]);
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) { return res.status(404).json({ message: 'Turnier nicht gefunden.' }); }
|
||||||
return res.status(404).json({ message: 'Turnier nicht gefunden.' });
|
|
||||||
}
|
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
} catch (error) {
|
} catch (error) { next(error); }
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a new tournament (Admin only - protected in router)
|
// Create a new tournament (Admin only - protected in router)
|
||||||
exports.createTournament = async (req, res, next) => {
|
exports.createTournament = async (req, res, next) => {
|
||||||
// Destructure expected fields from request body
|
const { name, date, location, description, logo_url, tournament_type, game_type, max_players, status } = req.body;
|
||||||
const {
|
if (!name || !tournament_type || !game_type) { return res.status(400).json({ message: 'Name, Turnier-Typ und Spiel-Typ sind erforderlich.' }); }
|
||||||
name,
|
const validTournamentTypes = ['knockout', 'group']; const validGameTypes = ['11_points', '21_points']; const validStatuses = ['planned', 'running', 'finished', 'cancelled'];
|
||||||
date,
|
if (!validTournamentTypes.includes(tournament_type)) { return res.status(400).json({ message: `Ungültiger Turnier-Typ: ${tournament_type}` }); }
|
||||||
location,
|
if (!validGameTypes.includes(game_type)) { return res.status(400).json({ message: `Ungültiger Spiel-Typ: ${game_type}` }); }
|
||||||
description,
|
const finalStatus = status && validStatuses.includes(status) ? status : 'planned';
|
||||||
logo_url, // Handle logo upload separately if needed
|
const finalMaxPlayers = max_players ? parseInt(max_players, 10) : null;
|
||||||
tournament_type,
|
if (max_players && (isNaN(finalMaxPlayers) || finalMaxPlayers < 0)) { return res.status(400).json({ message: 'Max. Spieler muss eine positive Zahl sein.' }); }
|
||||||
game_type,
|
const created_by = req.user.userId;
|
||||||
max_players,
|
|
||||||
status
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (!name || !tournament_type || !game_type) {
|
|
||||||
return res.status(400).json({ message: 'Name, Turnier-Typ und Spiel-Typ sind erforderlich.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate enum types
|
|
||||||
const validTournamentTypes = ['knockout', 'group'];
|
|
||||||
const validGameTypes = ['11_points', '21_points'];
|
|
||||||
const validStatuses = ['planned', 'running', 'finished', 'cancelled']; // Add valid statuses
|
|
||||||
if (!validTournamentTypes.includes(tournament_type)) {
|
|
||||||
return res.status(400).json({ message: `Ungültiger Turnier-Typ: ${tournament_type}` });
|
|
||||||
}
|
|
||||||
if (!validGameTypes.includes(game_type)) {
|
|
||||||
return res.status(400).json({ message: `Ungültiger Spiel-Typ: ${game_type}` });
|
|
||||||
}
|
|
||||||
const finalStatus = status && validStatuses.includes(status) ? status : 'planned'; // Default to 'planned' if invalid or missing
|
|
||||||
const finalMaxPlayers = max_players ? parseInt(max_players, 10) : null;
|
|
||||||
if (max_players && (isNaN(finalMaxPlayers) || finalMaxPlayers < 0)) {
|
|
||||||
return res.status(400).json({ message: 'Max. Spieler muss eine positive Zahl sein.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Get user ID from the authenticated request (set by authMiddleware)
|
|
||||||
const created_by = req.user.userId; // req.user is available because route is protected
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db.query(
|
const result = await db.query(
|
||||||
`INSERT INTO tournaments (name, date, location, description, logo_url, tournament_type, game_type, max_players, status, created_by)
|
`INSERT INTO tournaments (name, date, location, description, logo_url, tournament_type, game_type, max_players, status, created_by)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`,
|
||||||
RETURNING *`, // Return the newly created tournament
|
|
||||||
[name, date || null, location, description, logo_url, tournament_type, game_type, finalMaxPlayers, finalStatus, created_by]
|
[name, date || null, location, description, logo_url, tournament_type, game_type, finalMaxPlayers, finalStatus, created_by]
|
||||||
);
|
);
|
||||||
res.status(201).json(result.rows[0]);
|
res.status(201).json(result.rows[0]);
|
||||||
} catch (error) {
|
} catch (error) { next(error); }
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update an existing tournament (Admin only - protected in router)
|
// Update an existing tournament (Admin only - protected in router)
|
||||||
exports.updateTournament = async (req, res, next) => {
|
exports.updateTournament = async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
// Destructure fields that can be updated
|
const { name, date, location, description, logo_url, tournament_type, game_type, max_players, status } = req.body;
|
||||||
const {
|
const fieldsToUpdate = []; const values = []; let queryIndex = 1;
|
||||||
name,
|
|
||||||
date,
|
|
||||||
location,
|
|
||||||
description,
|
|
||||||
logo_url,
|
|
||||||
tournament_type,
|
|
||||||
game_type,
|
|
||||||
max_players,
|
|
||||||
status
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// Build the update query dynamically based on provided fields
|
|
||||||
const fieldsToUpdate = [];
|
|
||||||
const values = [];
|
|
||||||
let queryIndex = 1;
|
|
||||||
|
|
||||||
// Helper function to add field and value if value is provided
|
|
||||||
const addUpdate = (field, value) => {
|
const addUpdate = (field, value) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
let processedValue = value;
|
let processedValue = value;
|
||||||
// Validate enums/types if they are being updated
|
if (field === 'tournament_type' && !['knockout', 'group'].includes(value)) { throw new Error(`Ungültiger Turnier-Typ: ${value}`); }
|
||||||
if (field === 'tournament_type' && !['knockout', 'group'].includes(value)) {
|
if (field === 'game_type' && !['11_points', '21_points'].includes(value)) { throw new Error(`Ungültiger Spiel-Typ: ${value}`); }
|
||||||
throw new Error(`Ungültiger Turnier-Typ: ${value}`);
|
if (field === 'status' && !['planned', 'running', 'finished', 'cancelled'].includes(value)) { throw new Error(`Ungültiger Status: ${value}`); }
|
||||||
}
|
if (field === 'max_players') { processedValue = value ? parseInt(value, 10) : null; if (value && (isNaN(processedValue) || processedValue < 0)) { throw new Error('Max. Spieler muss eine positive Zahl sein.'); } }
|
||||||
if (field === 'game_type' && !['11_points', '21_points'].includes(value)) {
|
fieldsToUpdate.push(`${field} = $${queryIndex++}`); values.push(processedValue === '' ? null : processedValue);
|
||||||
throw new Error(`Ungültiger Spiel-Typ: ${value}`);
|
|
||||||
}
|
|
||||||
if (field === 'status' && !['planned', 'running', 'finished', 'cancelled'].includes(value)) {
|
|
||||||
throw new Error(`Ungültiger Status: ${value}`);
|
|
||||||
}
|
|
||||||
if (field === 'max_players') {
|
|
||||||
processedValue = value ? parseInt(value, 10) : null;
|
|
||||||
if (value && (isNaN(processedValue) || processedValue < 0)) {
|
|
||||||
throw new Error('Max. Spieler muss eine positive Zahl sein.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldsToUpdate.push(`${field} = $${queryIndex++}`);
|
|
||||||
// Handle empty strings potentially being passed for nullable fields
|
|
||||||
values.push(processedValue === '' ? null : processedValue);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addUpdate('name', name);
|
addUpdate('name', name); addUpdate('date', date); addUpdate('location', location); addUpdate('description', description); addUpdate('logo_url', logo_url);
|
||||||
addUpdate('date', date);
|
addUpdate('tournament_type', tournament_type); addUpdate('game_type', game_type); addUpdate('max_players', max_players); addUpdate('status', status);
|
||||||
addUpdate('location', location);
|
if (fieldsToUpdate.length === 0) { return res.status(400).json({ message: 'Keine Daten zum Aktualisieren angegeben.' }); }
|
||||||
addUpdate('description', description);
|
|
||||||
addUpdate('logo_url', logo_url);
|
|
||||||
addUpdate('tournament_type', tournament_type);
|
|
||||||
addUpdate('game_type', game_type);
|
|
||||||
addUpdate('max_players', max_players);
|
|
||||||
addUpdate('status', status);
|
|
||||||
|
|
||||||
if (fieldsToUpdate.length === 0) {
|
|
||||||
return res.status(400).json({ message: 'Keine Daten zum Aktualisieren angegeben.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the tournament ID for the WHERE clause
|
|
||||||
values.push(id);
|
values.push(id);
|
||||||
|
|
||||||
const updateQuery = `UPDATE tournaments SET ${fieldsToUpdate.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE tournament_id = $${queryIndex} RETURNING *`;
|
const updateQuery = `UPDATE tournaments SET ${fieldsToUpdate.join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE tournament_id = $${queryIndex} RETURNING *`;
|
||||||
|
|
||||||
const result = await db.query(updateQuery, values);
|
const result = await db.query(updateQuery, values);
|
||||||
|
if (result.rows.length === 0) { return res.status(404).json({ message: 'Turnier nicht gefunden.' }); }
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return res.status(404).json({ message: 'Turnier nicht gefunden.' });
|
|
||||||
}
|
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle potential validation errors from the helper
|
if (error.message.startsWith('Ungültiger') || error.message.includes('Spieler')) { return res.status(400).json({ message: error.message }); }
|
||||||
if (error.message.startsWith('Ungültiger') || error.message.includes('Spieler')) {
|
next(error);
|
||||||
return res.status(400).json({ message: error.message });
|
|
||||||
}
|
|
||||||
next(error); // Pass other errors to the handler
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete a tournament (Admin only - protected in router)
|
// Delete a tournament (Admin only - protected in router)
|
||||||
exports.deleteTournament = async (req, res, next) => {
|
exports.deleteTournament = async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
try {
|
try {
|
||||||
// ON DELETE CASCADE in the schema should handle related data (matches, tournament_players)
|
|
||||||
const result = await db.query('DELETE FROM tournaments WHERE tournament_id = $1 RETURNING tournament_id', [id]);
|
const result = await db.query('DELETE FROM tournaments WHERE tournament_id = $1 RETURNING tournament_id', [id]);
|
||||||
if (result.rowCount === 0) {
|
if (result.rowCount === 0) { return res.status(404).json({ message: 'Turnier nicht gefunden.' }); }
|
||||||
return res.status(404).json({ message: 'Turnier nicht gefunden.' });
|
res.status(204).send();
|
||||||
}
|
} catch (error) { next(error); }
|
||||||
res.status(204).send(); // No Content, successful deletion
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
// Get players participating in a specific tournament - includes gender
|
||||||
// NEW: Get players participating in a specific tournament
|
|
||||||
exports.getTournamentPlayers = async (req, res, next) => {
|
exports.getTournamentPlayers = async (req, res, next) => {
|
||||||
const { id } = req.params; // Tournament ID
|
const { id } = req.params; // Tournament ID
|
||||||
try {
|
try {
|
||||||
const result = await db.query(
|
const result = await db.query(
|
||||||
`SELECT p.player_id, p.first_name, p.last_name, p.club, p.qttr_points, p.age_class, tp.seed, tp.group_name, tp.status AS participation_status
|
`SELECT p.player_id, p.first_name, p.last_name, p.gender, p.club, p.qttr_points, p.age_class,
|
||||||
|
tp.seed, tp.group_name, tp.status AS participation_status
|
||||||
FROM players p
|
FROM players p
|
||||||
JOIN tournament_players tp ON p.player_id = tp.player_id
|
JOIN tournament_players tp ON p.player_id = tp.player_id
|
||||||
WHERE tp.tournament_id = $1
|
WHERE tp.tournament_id = $1
|
||||||
@@ -194,26 +95,92 @@ exports.getTournamentPlayers = async (req, res, next) => {
|
|||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
res.json(result.rows);
|
res.json(result.rows);
|
||||||
|
} catch (error) { next(error); }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- NEUE FUNKTIONEN ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt einen existierenden Spieler zu einem Turnier hinzu.
|
||||||
|
* Erwartet playerId und optional seed, group_name im Body.
|
||||||
|
* tournamentId kommt aus den URL-Parametern.
|
||||||
|
* Admin only - geschützt in der Route.
|
||||||
|
*/
|
||||||
|
exports.addPlayerToTournament = async (req, res, next) => {
|
||||||
|
const { tournamentId } = req.params;
|
||||||
|
const { playerId, seed, group_name } = req.body;
|
||||||
|
|
||||||
|
if (!playerId) {
|
||||||
|
return res.status(400).json({ message: 'playerId ist im Request Body erforderlich.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Validate if tournament and player actually exist before inserting?
|
||||||
|
// Could add checks here, but FK constraints will catch invalid IDs anyway.
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.query(
|
||||||
|
`INSERT INTO tournament_players (tournament_id, player_id, seed, group_name)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (tournament_id, player_id) DO NOTHING
|
||||||
|
RETURNING tournament_player_id`, // Check if something was returned
|
||||||
|
[tournamentId, playerId, seed || null, group_name || null]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length > 0) {
|
||||||
|
// Successfully inserted
|
||||||
|
res.status(201).json({ message: 'Spieler erfolgreich zum Turnier hinzugefügt.', added: true, detail: result.rows[0] });
|
||||||
|
} else {
|
||||||
|
// Conflict occurred, player was already in the tournament
|
||||||
|
res.status(200).json({ message: 'Spieler ist bereits in diesem Turnier registriert.', added: false });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Handle foreign key violation if tournamentId or playerId is invalid
|
||||||
|
if (error.code === '23503') {
|
||||||
|
return res.status(404).json({ message: `Turnier oder Spieler nicht gefunden. (${error.detail})` });
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt einen Spieler aus einem Turnier.
|
||||||
|
* tournamentId und playerId kommen aus den URL-Parametern.
|
||||||
|
* Admin only - geschützt in der Route.
|
||||||
|
*/
|
||||||
|
exports.removePlayerFromTournament = async (req, res, next) => {
|
||||||
|
const { tournamentId, playerId } = req.params;
|
||||||
|
|
||||||
|
if (!tournamentId || !playerId) {
|
||||||
|
// Should be guaranteed by route structure, but check anyway
|
||||||
|
return res.status(400).json({ message: 'tournamentId und playerId sind in der URL erforderlich.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if player is currently in a match that is not finished? Optional constraint.
|
||||||
|
|
||||||
|
const result = await db.query(
|
||||||
|
'DELETE FROM tournament_players WHERE tournament_id = $1 AND player_id = $2',
|
||||||
|
[tournamentId, playerId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
// No row was deleted, meaning the player wasn't in this tournament
|
||||||
|
return res.status(404).json({ message: 'Spieler wurde in diesem Turnier nicht gefunden.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successfully deleted
|
||||||
|
res.status(204).send(); // No Content
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Handle potential errors (though FK errors are less likely on DELETE here)
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- Placeholder functions for other features ---
|
// --- Placeholder functions --- (no changes)
|
||||||
|
exports.importTournaments = async (req, res, next) => { /* ... placeholder ... */ res.status(501).json({ message: 'Funktion noch nicht implementiert (Import).' }); };
|
||||||
|
exports.exportTournaments = async (req, res, next) => { /* ... placeholder ... */ res.status(501).json({ message: 'Funktion noch nicht implementiert (Export).' }); };
|
||||||
|
exports.addLogo = async (req, res, next) => { /* ... placeholder ... */ res.status(501).json({ message: 'Funktion noch nicht implementiert (Logo Upload).' }); };
|
||||||
|
|
||||||
exports.importTournaments = async (req, res, next) => {
|
|
||||||
// TODO: Implement CSV import logic using multer and csv-parser
|
|
||||||
res.status(501).json({ message: 'Funktion noch nicht implementiert (Import).' });
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.exportTournaments = async (req, res, next) => {
|
|
||||||
// TODO: Implement CSV export logic using fast-csv
|
|
||||||
res.status(501).json({ message: 'Funktion noch nicht implementiert (Export).' });
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.addLogo = async (req, res, next) => {
|
|
||||||
// TODO: Implement file upload logic using multer
|
|
||||||
// Update the tournament's logo_url field
|
|
||||||
res.status(501).json({ message: 'Funktion noch nicht implementiert (Logo Upload).' });
|
|
||||||
};
|
|
||||||
|
161
src/db/init.sql
161
src/db/init.sql
@@ -1,58 +1,45 @@
|
|||||||
-- Database Initialization Script for Tischtennis-Turnierverwaltung
|
-- Database Initialization Script for Tischtennis-Turnierverwaltung (v2)
|
||||||
|
|
||||||
-- Enable UUID generation
|
-- Enable UUID generation
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
-- User Roles Enum (optional but good practice)
|
-- User Roles Enum
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
CREATE TYPE user_role AS ENUM ('admin', 'referee', 'spectator');
|
CREATE TYPE user_role AS ENUM ('admin', 'referee', 'spectator');
|
||||||
EXCEPTION
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
-- Gender Enum (NEW)
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE player_gender AS ENUM ('m', 'w', 'd'); -- m: männlich, w: weiblich, d: divers
|
||||||
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
|
|
||||||
|
|
||||||
-- Users Table
|
-- Users Table
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
username VARCHAR(50) UNIQUE NOT NULL,
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
role user_role NOT NULL DEFAULT 'spectator', -- Default role
|
role user_role NOT NULL DEFAULT 'spectator',
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Function to update 'updated_at' timestamp
|
-- Function to update 'updated_at' timestamp
|
||||||
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
|
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql;
|
||||||
BEGIN
|
|
||||||
NEW.updated_at = NOW();
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
-- Trigger for users table
|
-- Trigger for users table
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TRIGGER set_timestamp_users BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamp();
|
||||||
CREATE TRIGGER set_timestamp_users
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
BEFORE UPDATE ON users
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION trigger_set_timestamp();
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null; -- Ignore if trigger already exists
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
|
|
||||||
-- Tournament Type Enum (KO or Group)
|
-- Tournament Type Enum
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TYPE tournament_type AS ENUM ('knockout', 'group');
|
||||||
CREATE TYPE tournament_type AS ENUM ('knockout', 'group');
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Game Type Enum (Points per set)
|
-- Game Type Enum
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TYPE game_type AS ENUM ('11_points', '21_points');
|
||||||
CREATE TYPE game_type AS ENUM ('11_points', '21_points');
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
|
|
||||||
-- Tournaments Table
|
-- Tournaments Table
|
||||||
@@ -62,61 +49,48 @@ CREATE TABLE IF NOT EXISTS tournaments (
|
|||||||
date DATE,
|
date DATE,
|
||||||
location VARCHAR(255),
|
location VARCHAR(255),
|
||||||
description TEXT,
|
description TEXT,
|
||||||
logo_url VARCHAR(255), -- URL or path to the logo file
|
logo_url VARCHAR(255),
|
||||||
tournament_type tournament_type NOT NULL DEFAULT 'knockout',
|
tournament_type tournament_type NOT NULL DEFAULT 'knockout',
|
||||||
game_type game_type NOT NULL DEFAULT '11_points',
|
game_type game_type NOT NULL DEFAULT '11_points',
|
||||||
max_players INTEGER,
|
max_players INTEGER,
|
||||||
status VARCHAR(50) DEFAULT 'planned', -- e.g., planned, running, finished, cancelled
|
status VARCHAR(50) DEFAULT 'planned', -- planned, running, finished, cancelled
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL -- Link to the admin who created it
|
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Trigger for tournaments table
|
-- Trigger for tournaments table
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TRIGGER set_timestamp_tournaments BEFORE UPDATE ON tournaments FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamp();
|
||||||
CREATE TRIGGER set_timestamp_tournaments
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
BEFORE UPDATE ON tournaments
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION trigger_set_timestamp();
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
|
|
||||||
-- Players Table
|
-- Players Table (Added gender)
|
||||||
CREATE TABLE IF NOT EXISTS players (
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
player_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
player_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
first_name VARCHAR(100) NOT NULL,
|
first_name VARCHAR(100) NOT NULL,
|
||||||
last_name VARCHAR(100) NOT NULL,
|
last_name VARCHAR(100) NOT NULL,
|
||||||
|
gender player_gender, -- NEW: Added gender column
|
||||||
club VARCHAR(150),
|
club VARCHAR(150),
|
||||||
qttr_points INTEGER,
|
qttr_points INTEGER,
|
||||||
age_class VARCHAR(50), -- e.g., U18, Herren A, Damen
|
age_class VARCHAR(50),
|
||||||
-- Add more fields as needed (e.g., date_of_birth, gender)
|
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Trigger for players table
|
-- Trigger for players table
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TRIGGER set_timestamp_players BEFORE UPDATE ON players FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamp();
|
||||||
CREATE TRIGGER set_timestamp_players
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
BEFORE UPDATE ON players
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION trigger_set_timestamp();
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
|
|
||||||
-- Tournament Players Table (Many-to-Many relationship)
|
-- Tournament Players Table (Many-to-Many relationship)
|
||||||
-- Links players to specific tournaments they are participating in
|
|
||||||
CREATE TABLE IF NOT EXISTS tournament_players (
|
CREATE TABLE IF NOT EXISTS tournament_players (
|
||||||
tournament_player_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
tournament_player_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
tournament_id UUID NOT NULL REFERENCES tournaments(tournament_id) ON DELETE CASCADE,
|
tournament_id UUID NOT NULL REFERENCES tournaments(tournament_id) ON DELETE CASCADE,
|
||||||
player_id UUID NOT NULL REFERENCES players(player_id) ON DELETE CASCADE,
|
player_id UUID NOT NULL REFERENCES players(player_id) ON DELETE CASCADE,
|
||||||
seed INTEGER, -- Optional seeding position
|
seed INTEGER,
|
||||||
group_name VARCHAR(50), -- For group stage
|
group_name VARCHAR(50),
|
||||||
status VARCHAR(50) DEFAULT 'registered', -- e.g., registered, active, eliminated
|
status VARCHAR(50) DEFAULT 'registered', -- registered, active, eliminated
|
||||||
UNIQUE (tournament_id, player_id), -- Ensure a player is only added once per tournament
|
UNIQUE (tournament_id, player_id),
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -125,59 +99,53 @@ CREATE TABLE IF NOT EXISTS tournament_players (
|
|||||||
CREATE TABLE IF NOT EXISTS matches (
|
CREATE TABLE IF NOT EXISTS matches (
|
||||||
match_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
match_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
tournament_id UUID NOT NULL REFERENCES tournaments(tournament_id) ON DELETE CASCADE,
|
tournament_id UUID NOT NULL REFERENCES tournaments(tournament_id) ON DELETE CASCADE,
|
||||||
round INTEGER, -- Round number (e.g., 1, 2, Quarterfinal=3, Semifinal=4, Final=5)
|
round INTEGER,
|
||||||
match_number_in_round INTEGER, -- Order within the round
|
match_number_in_round INTEGER,
|
||||||
player1_id UUID REFERENCES players(player_id) ON DELETE SET NULL, -- Reference to player 1
|
player1_id UUID REFERENCES players(player_id) ON DELETE SET NULL,
|
||||||
player2_id UUID REFERENCES players(player_id) ON DELETE SET NULL, -- Reference to player 2
|
player2_id UUID REFERENCES players(player_id) ON DELETE SET NULL,
|
||||||
winner_id UUID REFERENCES players(player_id) ON DELETE SET NULL, -- Determined after match completion
|
winner_id UUID REFERENCES players(player_id) ON DELETE SET NULL,
|
||||||
scheduled_time TIMESTAMPTZ,
|
scheduled_time TIMESTAMPTZ,
|
||||||
status VARCHAR(50) DEFAULT 'scheduled', -- e.g., scheduled, ongoing, finished, postponed
|
status VARCHAR(50) DEFAULT 'scheduled', -- scheduled, ongoing, finished, postponed
|
||||||
table_number VARCHAR(20), -- Which table the match is played on
|
table_number VARCHAR(20),
|
||||||
referee_id UUID REFERENCES users(user_id) ON DELETE SET NULL, -- Assigned referee
|
referee_id UUID REFERENCES users(user_id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
-- Add constraints or logic to ensure player1_id != player2_id if needed
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Trigger for matches table
|
-- Trigger for matches table
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TRIGGER set_timestamp_matches BEFORE UPDATE ON matches FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamp();
|
||||||
CREATE TRIGGER set_timestamp_matches
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
BEFORE UPDATE ON matches
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION trigger_set_timestamp();
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
|
|
||||||
-- Sets Table (Stores individual set scores for each match)
|
-- Sets Table
|
||||||
CREATE TABLE IF NOT EXISTS sets (
|
CREATE TABLE IF NOT EXISTS sets (
|
||||||
set_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
set_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
match_id UUID NOT NULL REFERENCES matches(match_id) ON DELETE CASCADE,
|
match_id UUID NOT NULL REFERENCES matches(match_id) ON DELETE CASCADE,
|
||||||
set_number INTEGER NOT NULL CHECK (set_number > 0), -- e.g., 1, 2, 3, 4, 5
|
set_number INTEGER NOT NULL CHECK (set_number > 0),
|
||||||
player1_score INTEGER NOT NULL CHECK (player1_score >= 0),
|
player1_score INTEGER NOT NULL CHECK (player1_score >= 0),
|
||||||
player2_score INTEGER NOT NULL CHECK (player2_score >= 0),
|
player2_score INTEGER NOT NULL CHECK (player2_score >= 0),
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE (match_id, set_number) -- Ensure set numbers are unique per match
|
UNIQUE (match_id, set_number)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
-- Notifications Table (For push notifications or general announcements)
|
-- Notifications Table
|
||||||
CREATE TABLE IF NOT EXISTS notifications (
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
notification_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
notification_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
tournament_id UUID REFERENCES tournaments(tournament_id) ON DELETE CASCADE, -- Optional: Link to a specific tournament
|
tournament_id UUID REFERENCES tournaments(tournament_id) ON DELETE CASCADE,
|
||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
type VARCHAR(50) DEFAULT 'info', -- e.g., info, warning, match_start, result_update
|
type VARCHAR(50) DEFAULT 'info',
|
||||||
is_sticky BOOLEAN DEFAULT FALSE, -- If true, needs manual dismissal
|
is_sticky BOOLEAN DEFAULT FALSE,
|
||||||
target_audience VARCHAR(50) DEFAULT 'all', -- e.g., all, spectators, referees, specific_player_id
|
target_audience VARCHAR(50) DEFAULT 'all',
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
expires_at TIMESTAMPTZ -- Optional: Auto-hide after this time
|
expires_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
-- Add Indexes for performance on frequently queried columns
|
-- Add Indexes for performance
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tournaments_date ON tournaments(date);
|
CREATE INDEX IF NOT EXISTS idx_tournaments_date ON tournaments(date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tournaments_status ON tournaments(status); -- Index status for finding active tournaments
|
||||||
CREATE INDEX IF NOT EXISTS idx_players_last_name ON players(last_name);
|
CREATE INDEX IF NOT EXISTS idx_players_last_name ON players(last_name);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tournament_players_tournament ON tournament_players(tournament_id);
|
CREATE INDEX IF NOT EXISTS idx_tournament_players_tournament ON tournament_players(tournament_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tournament_players_player ON tournament_players(player_id);
|
CREATE INDEX IF NOT EXISTS idx_tournament_players_player ON tournament_players(player_id);
|
||||||
@@ -186,7 +154,7 @@ CREATE INDEX IF NOT EXISTS idx_matches_status ON matches(status);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_sets_match ON sets(match_id);
|
CREATE INDEX IF NOT EXISTS idx_sets_match ON sets(match_id);
|
||||||
|
|
||||||
|
|
||||||
-- Optional: Table for storing settings like backup interval
|
-- Optional: Table for storing settings
|
||||||
CREATE TABLE IF NOT EXISTS application_settings (
|
CREATE TABLE IF NOT EXISTS application_settings (
|
||||||
setting_key VARCHAR(100) PRIMARY KEY,
|
setting_key VARCHAR(100) PRIMARY KEY,
|
||||||
setting_value TEXT,
|
setting_value TEXT,
|
||||||
@@ -195,21 +163,6 @@ CREATE TABLE IF NOT EXISTS application_settings (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Trigger for application_settings table
|
-- Trigger for application_settings table
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TRIGGER set_timestamp_application_settings BEFORE UPDATE ON application_settings FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamp();
|
||||||
CREATE TRIGGER set_timestamp_application_settings
|
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||||
BEFORE UPDATE ON application_settings
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION trigger_set_timestamp();
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- Example: Insert default backup setting if not present
|
|
||||||
-- INSERT INTO application_settings (setting_key, setting_value, description)
|
|
||||||
-- VALUES ('backup_interval_minutes', '1440', 'Interval for automated database backups in minutes.')
|
|
||||||
-- ON CONFLICT (setting_key) DO NOTHING;
|
|
||||||
|
|
||||||
|
|
||||||
-- Initial Data (Example: Add first admin user - handled by application logic now)
|
|
||||||
-- Consider adding default age classes or other lookup data here if needed.
|
|
||||||
|
|
||||||
|
@@ -11,10 +11,11 @@ const router = express.Router();
|
|||||||
router.get('/', tournamentController.getAllTournaments);
|
router.get('/', tournamentController.getAllTournaments);
|
||||||
|
|
||||||
// GET /api/tournaments/:id - Get a specific tournament
|
// GET /api/tournaments/:id - Get a specific tournament
|
||||||
router.get('/:id', tournamentController.getTournamentById);
|
// Needs to be before routes with more specific parameters like /export or /:id/players
|
||||||
|
router.get('/:id([0-9a-fA-F\\-]+)', tournamentController.getTournamentById); // Regex to ensure it's a UUID-like ID
|
||||||
|
|
||||||
// GET /api/tournaments/:id/players - Get players for a specific tournament (NEW)
|
// GET /api/tournaments/:id/players - Get players for a specific tournament
|
||||||
router.get('/:id/players', tournamentController.getTournamentPlayers);
|
router.get('/:tournamentId/players', tournamentController.getTournamentPlayers);
|
||||||
|
|
||||||
|
|
||||||
// --- Admin only routes (require authentication and 'admin' role) ---
|
// --- Admin only routes (require authentication and 'admin' role) ---
|
||||||
@@ -22,10 +23,19 @@ router.get('/:id/players', tournamentController.getTournamentPlayers);
|
|||||||
router.post('/', authenticateToken, authorizeRole('admin'), tournamentController.createTournament);
|
router.post('/', authenticateToken, authorizeRole('admin'), tournamentController.createTournament);
|
||||||
|
|
||||||
// PUT /api/tournaments/:id - Update a tournament
|
// PUT /api/tournaments/:id - Update a tournament
|
||||||
router.put('/:id', authenticateToken, authorizeRole('admin'), tournamentController.updateTournament);
|
router.put('/:id([0-9a-fA-F\\-]+)', authenticateToken, authorizeRole('admin'), tournamentController.updateTournament);
|
||||||
|
|
||||||
// DELETE /api/tournaments/:id - Delete a tournament
|
// DELETE /api/tournaments/:id - Delete a tournament
|
||||||
router.delete('/:id', authenticateToken, authorizeRole('admin'), tournamentController.deleteTournament);
|
router.delete('/:id([0-9a-fA-F\\-]+)', authenticateToken, authorizeRole('admin'), tournamentController.deleteTournament);
|
||||||
|
|
||||||
|
// --- NEW: Routes for managing tournament players (Admin only) ---
|
||||||
|
|
||||||
|
// POST /api/tournaments/:tournamentId/players - Add an existing player to a tournament
|
||||||
|
router.post('/:tournamentId/players', authenticateToken, authorizeRole('admin'), tournamentController.addPlayerToTournament);
|
||||||
|
|
||||||
|
// DELETE /api/tournaments/:tournamentId/players/:playerId - Remove a player from a tournament
|
||||||
|
router.delete('/:tournamentId/players/:playerId', authenticateToken, authorizeRole('admin'), tournamentController.removePlayerFromTournament);
|
||||||
|
|
||||||
|
|
||||||
// --- Placeholder routes for future implementation (Admin only) ---
|
// --- Placeholder routes for future implementation (Admin only) ---
|
||||||
|
|
||||||
@@ -33,11 +43,11 @@ router.delete('/:id', authenticateToken, authorizeRole('admin'), tournamentContr
|
|||||||
router.post('/import', authenticateToken, authorizeRole('admin'), tournamentController.importTournaments);
|
router.post('/import', authenticateToken, authorizeRole('admin'), tournamentController.importTournaments);
|
||||||
|
|
||||||
// GET /api/tournaments/export - Export tournaments to CSV
|
// GET /api/tournaments/export - Export tournaments to CSV
|
||||||
// Note: Might conflict with GET /:id if not handled carefully in controller or route order
|
// Placed after specific ID routes to avoid conflict
|
||||||
router.get('/export', authenticateToken, authorizeRole('admin'), tournamentController.exportTournaments);
|
router.get('/export', authenticateToken, authorizeRole('admin'), tournamentController.exportTournaments);
|
||||||
|
|
||||||
// POST /api/tournaments/:id/logo - Add/Update logo for a tournament
|
// POST /api/tournaments/:id/logo - Add/Update logo for a tournament
|
||||||
router.post('/:id/logo', authenticateToken, authorizeRole('admin'), tournamentController.addLogo);
|
router.post('/:id([0-9a-fA-F\\-]+)/logo', authenticateToken, authorizeRole('admin'), tournamentController.addLogo);
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
Reference in New Issue
Block a user