From 48cd0921df0a2a9947ca1a7eb73e3b7a3e9885ab Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 11 Sep 2025 14:11:19 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCgt=20die=20Methode=20listClubParticipati?= =?UTF-8?q?ons=20im=20OfficialTournamentController=20hinzu,=20um=20die=20T?= =?UTF-8?q?eilnahme=20von=20Mitgliedern=20an=20offiziellen=20Turnieren=20z?= =?UTF-8?q?u=20listen.=20Aktualisiert=20die=20Routen,=20um=20diese=20neue?= =?UTF-8?q?=20Funktionalit=C3=A4t=20zu=20integrieren.=20Verbessert=20die?= =?UTF-8?q?=20Benutzeroberfl=C3=A4che=20in=20OfficialTournaments.vue=20mit?= =?UTF-8?q?=20Tabs=20zur=20Anzeige=20von=20Veranstaltungen=20und=20Turnier?= =?UTF-8?q?beteiligungen=20sowie=20einer=20Filteroption=20f=C3=BCr=20den?= =?UTF-8?q?=20Zeitraum=20der=20Beteiligungen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../officialTournamentController.js | 106 +++++++ backend/routes/officialTournamentRoutes.js | 3 +- frontend/src/views/OfficialTournaments.vue | 267 ++++++++++++++++-- 3 files changed, 357 insertions(+), 19 deletions(-) diff --git a/backend/controllers/officialTournamentController.js b/backend/controllers/officialTournamentController.js index feb6281..cf88a4f 100644 --- a/backend/controllers/officialTournamentController.js +++ b/backend/controllers/officialTournamentController.js @@ -5,6 +5,8 @@ import { checkAccess } from '../utils/userUtils.js'; import OfficialTournament from '../models/OfficialTournament.js'; import OfficialCompetition from '../models/OfficialCompetition.js'; import OfficialCompetitionMember from '../models/OfficialCompetitionMember.js'; +import Member from '../models/Member.js'; +import { Op } from 'sequelize'; // In-Memory Store (einfacher Start); später DB-Modell const parsedTournaments = new Map(); // key: id, value: { id, clubId, rawText, parsedData } @@ -170,6 +172,110 @@ export const listOfficialTournaments = async (req, res) => { } }; +export const listClubParticipations = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId } = req.params; + await checkAccess(userToken, clubId); + const tournaments = await OfficialTournament.findAll({ where: { clubId } }); + if (!tournaments || tournaments.length === 0) return res.status(200).json([]); + const tournamentIds = tournaments.map(t => t.id); + + const rows = await OfficialCompetitionMember.findAll({ + where: { tournamentId: { [Op.in]: tournamentIds }, participated: true }, + include: [ + { model: OfficialCompetition, as: 'competition', attributes: ['id', 'tournamentId', 'ageClassCompetition', 'startTime'] }, + { model: OfficialTournament, as: 'tournament', attributes: ['id', 'title', 'eventDate'] }, + { model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }, + ] + }); + + const parseDmy = (s) => { + if (!s) return null; + const m = String(s).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/); + if (!m) return null; + const d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1])); + return isNaN(d.getTime()) ? null : d; + }; + const fmtDmy = (d) => { + const dd = String(d.getDate()).padStart(2, '0'); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const yyyy = d.getFullYear(); + return `${dd}.${mm}.${yyyy}`; + }; + + const byTournament = new Map(); + for (const r of rows) { + const t = r.tournament; + const c = r.competition; + const m = r.member; + if (!t || !c || !m) continue; + if (!byTournament.has(t.id)) { + byTournament.set(t.id, { + tournamentId: String(t.id), + title: t.title || null, + startDate: null, + endDate: null, + entries: [], + _dates: [], + _eventDate: t.eventDate || null, + }); + } + const bucket = byTournament.get(t.id); + const compDate = parseDmy(c.startTime || '') || null; + if (compDate) bucket._dates.push(compDate); + bucket.entries.push({ + memberId: m.id, + memberName: `${m.firstName || ''} ${m.lastName || ''}`.trim(), + competitionId: c.id, + competitionName: c.ageClassCompetition || '', + placement: r.placement || null, + date: compDate ? fmtDmy(compDate) : null, + }); + } + + const out = []; + for (const t of tournaments) { + const bucket = byTournament.get(t.id) || { + tournamentId: String(t.id), + title: t.title || null, + startDate: null, + endDate: null, + entries: [], + _dates: [], + _eventDate: t.eventDate || null, + }; + // Ableiten Start/Ende + if (bucket._dates.length) { + bucket._dates.sort((a, b) => a - b); + bucket.startDate = fmtDmy(bucket._dates[0]); + bucket.endDate = fmtDmy(bucket._dates[bucket._dates.length - 1]); + } else if (bucket._eventDate) { + const all = String(bucket._eventDate).match(/(\d{1,2}\.\d{1,2}\.\d{4})/g) || []; + if (all.length >= 1) { + const d1 = parseDmy(all[0]); + const d2 = all.length >= 2 ? parseDmy(all[1]) : d1; + if (d1) bucket.startDate = fmtDmy(d1); + if (d2) bucket.endDate = fmtDmy(d2); + } + } + // Sort entries: Mitglied, dann Konkurrenz + bucket.entries.sort((a, b) => { + const mcmp = (a.memberName || '').localeCompare(b.memberName || '', 'de', { sensitivity: 'base' }); + if (mcmp !== 0) return mcmp; + return (a.competitionName || '').localeCompare(b.competitionName || '', 'de', { sensitivity: 'base' }); + }); + delete bucket._dates; + delete bucket._eventDate; + out.push(bucket); + } + + res.status(200).json(out); + } catch (e) { + res.status(500).json({ error: 'Failed to list club participations' }); + } +}; + export const deleteOfficialTournament = async (req, res) => { try { const { authcode: userToken } = req.headers; diff --git a/backend/routes/officialTournamentRoutes.js b/backend/routes/officialTournamentRoutes.js index e2fad46..2a3c012 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 } from '../controllers/officialTournamentController.js'; +import { uploadTournamentPdf, getParsedTournament, listOfficialTournaments, deleteOfficialTournament, upsertCompetitionMember, listClubParticipations } from '../controllers/officialTournamentController.js'; const router = express.Router(); const upload = multer({ storage: multer.memoryStorage() }); @@ -9,6 +9,7 @@ const upload = multer({ storage: multer.memoryStorage() }); router.use(authenticate); router.get('/:clubId', listOfficialTournaments); +router.get('/:clubId/participations/summary', listClubParticipations); router.post('/:clubId/upload', upload.single('pdf'), uploadTournamentPdf); router.get('/:clubId/:id', getParsedTournament); router.delete('/:clubId/:id', deleteOfficialTournament); diff --git a/frontend/src/views/OfficialTournaments.vue b/frontend/src/views/OfficialTournaments.vue index 0c89b28..64c37d7 100644 --- a/frontend/src/views/OfficialTournaments.vue +++ b/frontend/src/views/OfficialTournaments.vue @@ -2,17 +2,22 @@

Offizielle Turniere

-

Gespeicherte Veranstaltungen

- -
+
+ + +
+
+

Gespeicherte Veranstaltungen

+ +
@@ -48,6 +53,11 @@
+
+ + +
+

Konkurrenzen

@@ -62,7 +72,7 @@
+
+
+

Ergebnisse

+ + + + + + + + + + + + + + + + + + + + + +
MitgliedKonkurrenzStartzeitAngemeldetTeilgenommenPlatzierung
{{ row.memberName }}{{ row.competitionName }}{{ row.start }}{{ row.registered ? 'Ja' : 'Nein' }}{{ row.participated ? 'Ja' : 'Nein' }}{{ row.placement || '–' }}
+
+
+
+
+

Turnierbeteiligungen

+
+ + +
+ + + + + + + + + + + + + + + + + +
MitgliedKonkurrenzDatumPlatzierung
{{ row.memberName }}{{ row.competitionName }}{{ row.date }}{{ row.placement || '–' }}
+