Fügt Unterstützung für offizielle Turniere und Wettbewerbe hinzu. Aktualisiert die Datenbankmodelle, um Geschlecht für Mitglieder zu erfassen, und implementiert neue Routen sowie Frontend-Komponenten zur Anzeige und Verwaltung dieser Daten. Verbessert die Benutzeroberfläche zur Eingabe von Mitgliederdaten und aktualisiert die Abhängigkeiten im Projekt.
This commit is contained in:
@@ -34,11 +34,11 @@ const getWaitingApprovals = async(req, res) => {
|
||||
const setClubMembers = async (req, res) => {
|
||||
try {
|
||||
const { id: memberId, firstname: firstName, lastname: lastName, street, city, birthdate, phone, email, active,
|
||||
testMembership, picsInInternetAllowed } = req.body;
|
||||
testMembership, picsInInternetAllowed, gender } = req.body;
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate,
|
||||
phone, email, active, testMembership, picsInInternetAllowed);
|
||||
phone, email, active, testMembership, picsInInternetAllowed, gender);
|
||||
res.status(addResult.status || 500).json(addResult.response);
|
||||
} catch (error) {
|
||||
console.error('[setClubMembers] - Error:', error);
|
||||
|
||||
319
backend/controllers/officialTournamentController.js
Normal file
319
backend/controllers/officialTournamentController.js
Normal file
@@ -0,0 +1,319 @@
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const pdfParse = require('pdf-parse/lib/pdf-parse.js');
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import OfficialTournament from '../models/OfficialTournament.js';
|
||||
import OfficialCompetition from '../models/OfficialCompetition.js';
|
||||
|
||||
// In-Memory Store (einfacher Start); später DB-Modell
|
||||
const parsedTournaments = new Map(); // key: id, value: { id, clubId, rawText, parsedData }
|
||||
let seq = 1;
|
||||
|
||||
export const uploadTournamentPdf = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
if (!req.file || !req.file.buffer) return res.status(400).json({ error: 'No pdf provided' });
|
||||
const data = await pdfParse(req.file.buffer);
|
||||
const parsed = parseTournamentText(data.text);
|
||||
const t = await OfficialTournament.create({
|
||||
clubId,
|
||||
title: parsed.title || null,
|
||||
eventDate: parsed.termin || null,
|
||||
organizer: null,
|
||||
host: null,
|
||||
venues: JSON.stringify(parsed.austragungsorte || []),
|
||||
competitionTypes: JSON.stringify(parsed.konkurrenztypen || []),
|
||||
registrationDeadlines: JSON.stringify(parsed.meldeschluesse || []),
|
||||
});
|
||||
// competitions persistieren
|
||||
for (const c of parsed.competitions || []) {
|
||||
// Korrigiere Fehlzuordnung: Wenn die Zeile mit "Stichtag" fälschlich in performanceClass steht
|
||||
let performanceClass = c.leistungsklasse || c.performanceClass || null;
|
||||
let cutoffDate = c.stichtag || c.cutoffDate || null;
|
||||
if (performanceClass && /^stichtag\b/i.test(performanceClass)) {
|
||||
cutoffDate = performanceClass.replace(/^stichtag\s*:?\s*/i, '').trim();
|
||||
performanceClass = null;
|
||||
}
|
||||
await OfficialCompetition.create({
|
||||
tournamentId: t.id,
|
||||
ageClassCompetition: c.altersklasseWettbewerb || c.ageClassCompetition || null,
|
||||
performanceClass,
|
||||
startTime: c.startzeit || c.startTime || null,
|
||||
registrationDeadlineDate: c.meldeschlussDatum || c.registrationDeadlineDate || null,
|
||||
registrationDeadlineOnline: c.meldeschlussOnline || c.registrationDeadlineOnline || null,
|
||||
cutoffDate,
|
||||
ttrRelevant: c.ttrRelevant || null,
|
||||
openTo: c.offenFuer || c.openTo || null,
|
||||
preliminaryRound: c.vorrunde || c.preliminaryRound || null,
|
||||
finalRound: c.endrunde || c.finalRound || null,
|
||||
maxParticipants: c.maxTeilnehmer || c.maxParticipants || null,
|
||||
entryFee: c.startgeld || c.entryFee || null,
|
||||
});
|
||||
}
|
||||
res.status(201).json({ id: String(t.id) });
|
||||
} catch (e) {
|
||||
console.error('[uploadTournamentPdf] Error:', e);
|
||||
res.status(500).json({ error: 'Failed to parse pdf' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getParsedTournament = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, id } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await OfficialTournament.findOne({ where: { id, clubId } });
|
||||
if (!t) return res.status(404).json({ error: 'not found' });
|
||||
const comps = await OfficialCompetition.findAll({ where: { tournamentId: id } });
|
||||
const competitions = comps.map((c) => {
|
||||
const j = c.toJSON();
|
||||
return {
|
||||
id: j.id,
|
||||
tournamentId: j.tournamentId,
|
||||
ageClassCompetition: j.ageClassCompetition || null,
|
||||
performanceClass: j.performanceClass || null,
|
||||
startTime: j.startTime || null,
|
||||
registrationDeadlineDate: j.registrationDeadlineDate || null,
|
||||
registrationDeadlineOnline: j.registrationDeadlineOnline || null,
|
||||
cutoffDate: j.cutoffDate || null,
|
||||
ttrRelevant: j.ttrRelevant || null,
|
||||
openTo: j.openTo || null,
|
||||
preliminaryRound: j.preliminaryRound || null,
|
||||
finalRound: j.finalRound || null,
|
||||
maxParticipants: j.maxParticipants || null,
|
||||
entryFee: j.entryFee || null,
|
||||
// Legacy Felder zusätzlich, falls Frontend sie noch nutzt
|
||||
altersklasseWettbewerb: j.ageClassCompetition || null,
|
||||
leistungsklasse: j.performanceClass || null,
|
||||
startzeit: j.startTime || null,
|
||||
meldeschlussDatum: j.registrationDeadlineDate || null,
|
||||
meldeschlussOnline: j.registrationDeadlineOnline || null,
|
||||
stichtag: j.cutoffDate || null,
|
||||
offenFuer: j.openTo || null,
|
||||
vorrunde: j.preliminaryRound || null,
|
||||
endrunde: j.finalRound || null,
|
||||
maxTeilnehmer: j.maxParticipants || null,
|
||||
startgeld: j.entryFee || null,
|
||||
};
|
||||
});
|
||||
res.status(200).json({
|
||||
id: String(t.id),
|
||||
clubId: String(t.clubId),
|
||||
parsedData: {
|
||||
title: t.title,
|
||||
termin: t.eventDate,
|
||||
austragungsorte: JSON.parse(t.venues || '[]'),
|
||||
konkurrenztypen: JSON.parse(t.competitionTypes || '[]'),
|
||||
meldeschluesse: JSON.parse(t.registrationDeadlines || '[]'),
|
||||
competitions,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to fetch parsed tournament' });
|
||||
}
|
||||
};
|
||||
|
||||
export const listOfficialTournaments = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const list = await OfficialTournament.findAll({ where: { clubId } });
|
||||
res.status(200).json(list);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to list tournaments' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteOfficialTournament = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, id } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const t = await OfficialTournament.findOne({ where: { id, clubId } });
|
||||
if (!t) return res.status(404).json({ error: 'not found' });
|
||||
await OfficialCompetition.destroy({ where: { tournamentId: id } });
|
||||
await OfficialTournament.destroy({ where: { id } });
|
||||
res.status(204).send();
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to delete tournament' });
|
||||
}
|
||||
};
|
||||
|
||||
function parseTournamentText(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const normLines = lines.map(l => l.replace(/\s+/g, ' ').trim());
|
||||
|
||||
const findTitle = () => {
|
||||
const idx = normLines.findIndex(l => /Kreiseinzelmeisterschaften/i.test(l));
|
||||
return idx >= 0 ? normLines[idx] : null;
|
||||
};
|
||||
|
||||
const extractBlockAfter = (labels, multiline = false) => {
|
||||
const idx = normLines.findIndex(l => labels.some(lb => l.toLowerCase().startsWith(lb)));
|
||||
if (idx === -1) return multiline ? [] : null;
|
||||
const line = normLines[idx];
|
||||
const afterColon = line.includes(':') ? line.split(':').slice(1).join(':').trim() : '';
|
||||
if (!multiline) {
|
||||
if (afterColon) return afterColon;
|
||||
// sonst nächste nicht-leere Zeile
|
||||
for (let i = idx + 1; i < normLines.length; i++) {
|
||||
if (normLines[i]) return normLines[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// multiline bis zur nächsten Leerzeile oder nächsten bekannten Section
|
||||
const out = [];
|
||||
if (afterColon) out.push(afterColon);
|
||||
for (let i = idx + 1; i < normLines.length; i++) {
|
||||
const ln = normLines[i];
|
||||
if (!ln) break;
|
||||
if (/^(termin|austragungsort|austragungsorte|konkurrenz|konkurrenzen|konkurrenztypen|meldeschluss|altersklassen|startzeiten)/i.test(ln)) break;
|
||||
out.push(ln);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const extractAllMatches = (regex) => {
|
||||
const results = [];
|
||||
for (const l of normLines) {
|
||||
const m = l.match(regex);
|
||||
if (m) results.push(m);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
const title = findTitle();
|
||||
const termin = extractBlockAfter(['termin', 'termin '], false);
|
||||
const austragungsorte = extractBlockAfter(['austragungsort', 'austragungsorte'], true);
|
||||
let konkurrenzRaw = extractBlockAfter(['konkurrenz', 'konkurrenzen', 'konkurrenztypen'], true);
|
||||
if (konkurrenzRaw && !Array.isArray(konkurrenzRaw)) konkurrenzRaw = [konkurrenzRaw];
|
||||
const konkurrenztypen = (konkurrenzRaw || []).flatMap(l => l.split(/[;,]/)).map(s => s.trim()).filter(Boolean);
|
||||
|
||||
// Meldeschlüsse mit Position und Zuordnung zu AK ermitteln
|
||||
const meldeschluesseRaw = [];
|
||||
for (let i = 0; i < normLines.length; i++) {
|
||||
const l = normLines[i];
|
||||
const m = l.match(/meldeschluss\s*:?\s*(.+)$/i);
|
||||
if (m) meldeschluesseRaw.push({ line: i, value: m[1].trim() });
|
||||
}
|
||||
|
||||
let altersRaw = extractBlockAfter(['altersklassen', 'altersklasse'], true);
|
||||
if (altersRaw && !Array.isArray(altersRaw)) altersRaw = [altersRaw];
|
||||
const altersklassen = (altersRaw || []).flatMap(l => l.split(/[;,]/)).map(s => s.trim()).filter(Boolean);
|
||||
|
||||
// Wettbewerbe/Konkurrenzen parsen (Block ab "3. Konkurrenzen")
|
||||
const competitions = [];
|
||||
const konkIdx = normLines.findIndex(l => /^\s*3\.?\s+Konkurrenzen/i.test(l) || /^Konkurrenzen\b/i.test(l));
|
||||
// Bestimme Start-Sektionsnummer (z. B. 3 bei "3. Konkurrenzen"), fallback 3
|
||||
const startSectionNum = (() => {
|
||||
if (konkIdx === -1) return 3;
|
||||
const m = normLines[konkIdx].match(/^\s*(\d+)\./);
|
||||
return m ? parseInt(m[1], 10) : 3;
|
||||
})();
|
||||
const nextSectionIdx = () => {
|
||||
for (let i = konkIdx + 1; i < normLines.length; i++) {
|
||||
const m = normLines[i].match(/^\s*(\d+)\.\s+/);
|
||||
if (m) {
|
||||
const num = parseInt(m[1], 10);
|
||||
if (!Number.isNaN(num) && num > startSectionNum) return i;
|
||||
}
|
||||
// Hinweis: Seitenfußzeilen wie "nu.Dokument ..." ignorieren wir, damit mehrseitige Blöcke nicht abbrechen
|
||||
}
|
||||
return normLines.length;
|
||||
};
|
||||
if (konkIdx !== -1) {
|
||||
const endIdx = nextSectionIdx();
|
||||
let i = konkIdx + 1;
|
||||
while (i < endIdx) {
|
||||
const line = normLines[i];
|
||||
if (/^Altersklasse\/Wettbewerb\s*:/i.test(line)) {
|
||||
const comp = {};
|
||||
comp.altersklasseWettbewerb = line.split(':').slice(1).join(':').trim();
|
||||
i++;
|
||||
while (i < endIdx && !/^Altersklasse\/Wettbewerb\s*:/i.test(normLines[i])) {
|
||||
const ln = normLines[i];
|
||||
const m = ln.match(/^([^:]+):\s*(.*)$/);
|
||||
if (m) {
|
||||
const key = m[1].trim().toLowerCase();
|
||||
const val = m[2].trim();
|
||||
if (key.startsWith('leistungsklasse')) comp.leistungsklasse = val;
|
||||
else if (key === 'startzeit') {
|
||||
// Erwartet: 20.09.2025 13:30 Uhr -> wir extrahieren Datum+Zeit
|
||||
const sm = val.match(/(\d{2}\.\d{2}\.\d{4})\s+(\d{1,2}:\d{2})/);
|
||||
comp.startzeit = sm ? `${sm[1]} ${sm[2]}` : val;
|
||||
}
|
||||
else if (key.startsWith('meldeschluss datum')) comp.meldeschlussDatum = val;
|
||||
else if (key.startsWith('meldeschluss online')) comp.meldeschlussOnline = val;
|
||||
else if (key === 'stichtag') comp.stichtag = val;
|
||||
else if (key === 'ttr-relevant') comp.ttrRelevant = val;
|
||||
else if (key === 'offen für') comp.offenFuer = val;
|
||||
else if (key.startsWith('austragungssys. vorrunde')) comp.vorrunde = val;
|
||||
else if (key.startsWith('austragungssys. endrunde')) comp.endrunde = val;
|
||||
else if (key.startsWith('max. teilnehmerzahl')) comp.maxTeilnehmer = val;
|
||||
else if (key === 'startgeld') comp.startgeld = val;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
competitions.push(comp);
|
||||
continue; // schon auf nächster Zeile
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Altersklassen-Positionen im Text (zur Zuordnung von Meldeschlüssen)
|
||||
const akPositions = [];
|
||||
for (let i = 0; i < normLines.length; i++) {
|
||||
const l = normLines[i];
|
||||
const m = l.match(/\b(U\d+|AK\s*\d+)\b/i);
|
||||
if (m) akPositions.push({ line: i, ak: m[1].toUpperCase().replace(/\s+/g, '') });
|
||||
}
|
||||
|
||||
const meldeschluesseByAk = {};
|
||||
for (const ms of meldeschluesseRaw) {
|
||||
// Nächste AK im Umkreis von 3 Zeilen suchen
|
||||
let best = null;
|
||||
let bestDist = Infinity;
|
||||
for (const ak of akPositions) {
|
||||
const dist = Math.abs(ak.line - ms.line);
|
||||
if (dist < bestDist && dist <= 3) { best = ak; bestDist = dist; }
|
||||
}
|
||||
if (best) {
|
||||
if (!meldeschluesseByAk[best.ak]) meldeschluesseByAk[best.ak] = new Set();
|
||||
meldeschluesseByAk[best.ak].add(ms.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Dedup global
|
||||
const meldeschluesse = Array.from(new Set(meldeschluesseRaw.map(x => x.value)));
|
||||
// Sets zu Arrays
|
||||
const meldeschluesseByAkOut = Object.fromEntries(Object.entries(meldeschluesseByAk).map(([k,v]) => [k, Array.from(v)]));
|
||||
|
||||
// Vorhandene einfache Personenerkennung (optional, zu Analysezwecken)
|
||||
const entries = [];
|
||||
for (const l of normLines) {
|
||||
const m = l.match(/^([A-Za-zÄÖÜäöüß\-\s']{3,})(?:\s+\((m|w|d)\))?$/i);
|
||||
if (m && /\s/.test(m[1])) {
|
||||
entries.push({ name: m[1].trim(), genderHint: m[2] || null });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
termin,
|
||||
austragungsorte,
|
||||
konkurrenztypen,
|
||||
meldeschluesse,
|
||||
meldeschluesseByAk: meldeschluesseByAkOut,
|
||||
altersklassen,
|
||||
startzeiten: {},
|
||||
competitions,
|
||||
entries,
|
||||
debug: { normLines },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user