diff --git a/backend/controllers/officialTournamentController.js b/backend/controllers/officialTournamentController.js index cf88a4f..9e1db96 100644 --- a/backend/controllers/officialTournamentController.js +++ b/backend/controllers/officialTournamentController.js @@ -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; diff --git a/backend/routes/officialTournamentRoutes.js b/backend/routes/officialTournamentRoutes.js index 2a3c012..16d1108 100644 --- a/backend/routes/officialTournamentRoutes.js +++ b/backend/routes/officialTournamentRoutes.js @@ -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; diff --git a/frontend/src/views/OfficialTournaments.vue b/frontend/src/views/OfficialTournaments.vue index a117abf..ebdded5 100644 --- a/frontend/src/views/OfficialTournaments.vue +++ b/frontend/src/views/OfficialTournaments.vue @@ -163,8 +163,8 @@ Mitglied Konkurrenz Startzeit - Angemeldet - Teilgenommen + Status + Aktion Platzierung @@ -175,9 +175,53 @@ {{ group.memberName }} {{ item.competitionName }} {{ item.start }} - {{ item.registered ? 'Ja' : 'Nein' }} - {{ item.participated ? 'Ja' : 'Nein' }} - {{ item.placement || '–' }} + + Hat gespielt + Angemeldet + Möchte teilnehmen + Nicht interessiert + + + + + + + + + + {{ item.placement || '–' }} + @@ -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); +}