Fügt die Funktionalität zur Aktualisierung des Teilnehmerstatus in officialTournamentController.js hinzu. Implementiert die Route zum Aktualisieren des Status eines Teilnehmers in officialTournamentRoutes.js und passt die Benutzeroberfläche in OfficialTournaments.vue an, um den neuen Status anzuzeigen und Aktionen wie Anmelden, Teilnehmen und Zurücksetzen zu ermöglichen.

This commit is contained in:
Torsten Schulz (local)
2025-09-21 18:05:50 +02:00
parent 0ee16c7766
commit e4fcf2eca2
3 changed files with 259 additions and 6 deletions

View File

@@ -160,6 +160,71 @@ export const upsertCompetitionMember = async (req, res) => {
}
};
export const updateParticipantStatus = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, id } = req.params; // id = tournamentId
await checkAccess(userToken, clubId);
const { competitionId, memberId, action } = req.body;
if (!competitionId || !memberId || !action) {
return res.status(400).json({ error: 'competitionId, memberId and action required' });
}
const [row] = await OfficialCompetitionMember.findOrCreate({
where: { competitionId, memberId },
defaults: {
tournamentId: id,
competitionId,
memberId,
wants: false,
registered: false,
participated: false,
placement: null,
}
});
// Status-Update basierend auf Aktion
switch (action) {
case 'register':
// Von "möchte teilnehmen" zu "angemeldet"
row.wants = true;
row.registered = true;
row.participated = false;
break;
case 'participate':
// Von "angemeldet" zu "hat gespielt"
row.wants = true;
row.registered = true;
row.participated = true;
break;
case 'reset':
// Zurück zu "möchte teilnehmen"
row.wants = true;
row.registered = false;
row.participated = false;
break;
default:
return res.status(400).json({ error: 'Invalid action. Use: register, participate, or reset' });
}
await row.save();
return res.status(200).json({
success: true,
id: row.id,
status: {
wants: row.wants,
registered: row.registered,
participated: row.participated,
placement: row.placement
}
});
} catch (e) {
console.error('[updateParticipantStatus] Error:', e);
res.status(500).json({ error: 'Failed to update participant status' });
}
};
export const listOfficialTournaments = async (req, res) => {
try {
const { authcode: userToken } = req.headers;

View File

@@ -1,7 +1,7 @@
import express from 'express';
import multer from 'multer';
import { authenticate } from '../middleware/authMiddleware.js';
import { uploadTournamentPdf, getParsedTournament, listOfficialTournaments, deleteOfficialTournament, upsertCompetitionMember, listClubParticipations } from '../controllers/officialTournamentController.js';
import { uploadTournamentPdf, getParsedTournament, listOfficialTournaments, deleteOfficialTournament, upsertCompetitionMember, listClubParticipations, updateParticipantStatus } from '../controllers/officialTournamentController.js';
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
@@ -14,6 +14,7 @@ router.post('/:clubId/upload', upload.single('pdf'), uploadTournamentPdf);
router.get('/:clubId/:id', getParsedTournament);
router.delete('/:clubId/:id', deleteOfficialTournament);
router.post('/:clubId/:id/participation', upsertCompetitionMember);
router.post('/:clubId/:id/status', updateParticipantStatus);
export default router;

View File

@@ -163,8 +163,8 @@
<th>Mitglied</th>
<th>Konkurrenz</th>
<th>Startzeit</th>
<th>Angemeldet</th>
<th>Teilgenommen</th>
<th>Status</th>
<th>Aktion</th>
<th>Platzierung</th>
</tr>
</thead>
@@ -175,9 +175,53 @@
<td v-if="idx === 0" :rowspan="group.items.length" class="member-cell">{{ group.memberName }}</td>
<td class="indented">{{ item.competitionName }}</td>
<td>{{ item.start }}</td>
<td>{{ item.registered ? 'Ja' : 'Nein' }}</td>
<td>{{ item.participated ? 'Ja' : 'Nein' }}</td>
<td>{{ item.placement || '' }}</td>
<td class="status-cell">
<span v-if="item.participated" class="status-badge status-played">Hat gespielt</span>
<span v-else-if="item.registered" class="status-badge status-registered">Angemeldet</span>
<span v-else-if="item.wants" class="status-badge status-wants">Möchte teilnehmen</span>
<span v-else class="status-badge status-none">Nicht interessiert</span>
</td>
<td class="action-cell">
<button
v-if="!item.participated && !item.registered && item.wants"
@click="updateStatus(item, 'register')"
class="btn-status btn-register"
title="Als angemeldet markieren">
Anmelden
</button>
<button
v-else-if="item.registered && !item.participated"
@click="updateStatus(item, 'participate')"
class="btn-status btn-participate"
title="Als teilgenommen markieren">
Teilgenommen
</button>
<button
v-else-if="item.participated"
@click="updateStatus(item, 'reset')"
class="btn-status btn-reset"
title="Status zurücksetzen">
Zurücksetzen
</button>
<button
v-else
@click="updateStatus(item, 'register')"
class="btn-status btn-register"
title="Teilnahme anmelden">
Anmelden
</button>
</td>
<td>
<input
v-if="item.participated"
type="text"
:value="item.placement || ''"
@change="updatePlacement(item, $event.target.value)"
placeholder="z.B. 3. Platz"
class="placement-input"
/>
<span v-else>{{ item.placement || '' }}</span>
</td>
</tr>
</template>
</template>
@@ -756,6 +800,43 @@ export default {
this.getParticipation(c.id, m.id).placement = v || null;
this.saveParticipation(c.id, m.id);
},
async updateStatus(item, action) {
try {
const [competitionId, memberId] = item.key.split('-');
const response = await apiClient.post(`/official-tournaments/${this.currentClub}/${this.uploadedId}/status`, {
competitionId: parseInt(competitionId),
memberId: parseInt(memberId),
action: action
});
if (response.data.success) {
// Aktualisiere den lokalen Status
const key = `${competitionId}-${memberId}`;
const participation = this.getParticipation(competitionId, memberId);
participation.wants = response.data.status.wants;
participation.registered = response.data.status.registered;
participation.participated = response.data.status.participated;
participation.placement = response.data.status.placement;
// Aktualisiere auch die participationMap
this.participationMap[key] = participation;
}
} catch (error) {
console.error('Fehler beim Aktualisieren des Status:', error);
alert('Fehler beim Aktualisieren des Status: ' + (error.response?.data?.error || error.message));
}
},
async updatePlacement(item, value) {
try {
const [competitionId, memberId] = item.key.split('-');
const participation = this.getParticipation(competitionId, memberId);
participation.placement = value.trim() || null;
await this.saveParticipation(competitionId, memberId);
} catch (error) {
console.error('Fehler beim Aktualisieren der Platzierung:', error);
alert('Fehler beim Aktualisieren der Platzierung: ' + (error.response?.data?.error || error.message));
}
},
// Auswahl Helfer + PDF-Generierung
openMemberDialog() { this.showMemberDialog = true; },
closeMemberDialog() { this.showMemberDialog = false; },
@@ -1024,6 +1105,112 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali
.dialog-col h4 { margin: 0 0 .5rem 0; }
.members-col .check-item span.active { font-weight: bold; }
.recommendations-col .check-item { padding: .15rem 0; }
/* Status Management Styles */
.status-cell {
text-align: center;
vertical-align: middle;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
text-align: center;
min-width: 120px;
}
.status-badge.status-played {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-badge.status-registered {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.status-badge.status-wants {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.status-badge.status-none {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.action-cell {
text-align: center;
vertical-align: middle;
}
.btn-status {
padding: 0.4rem 0.8rem;
border: none;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.5s ease;
min-width: 100px;
}
.btn-status:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.btn-status:active {
transform: translateY(0);
}
.btn-register {
background-color: #007bff;
color: white;
}
.btn-register:hover {
background-color: #0056b3;
}
.btn-participate {
background-color: #28a745;
color: white;
}
.btn-participate:hover {
background-color: #1e7e34;
}
.btn-reset {
background-color: #6c757d;
color: white;
}
.btn-reset:hover {
background-color: #545b62;
}
.placement-input {
width: 120px;
padding: 0.25rem 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 0.85rem;
}
.placement-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
}
</style>