Fügt die Unterstützung für Teilnahmegebühren in officialTournamentController.js hinzu, einschließlich der Extraktion von Gebühren aus dem Turniertext. Aktualisiert das Datenmodell in OfficialTournament.js, um die Teilnahmegebühren zu speichern. Passt die Benutzeroberfläche in OfficialTournaments.vue an, um die Teilnahmegebühren anzuzeigen, und aktualisiert PDFGenerator.js, um die Gebühren im PDF-Dokument darzustellen.

This commit is contained in:
Torsten Schulz (local)
2025-09-21 18:39:25 +02:00
parent a36f0ea446
commit adb93af906
4 changed files with 163 additions and 8 deletions

View File

@@ -29,6 +29,7 @@ export const uploadTournamentPdf = async (req, res) => {
venues: JSON.stringify(parsed.austragungsorte || []),
competitionTypes: JSON.stringify(parsed.konkurrenztypen || []),
registrationDeadlines: JSON.stringify(parsed.meldeschluesse || []),
entryFees: JSON.stringify(parsed.entryFees || {}),
});
// competitions persistieren
for (const c of parsed.competitions || []) {
@@ -111,6 +112,7 @@ export const getParsedTournament = async (req, res) => {
austragungsorte: JSON.parse(t.venues || '[]'),
konkurrenztypen: JSON.parse(t.competitionTypes || '[]'),
meldeschluesse: JSON.parse(t.registrationDeadlines || '[]'),
entryFees: JSON.parse(t.entryFees || '{}'),
competitions,
},
participation: entries.map(e => ({
@@ -365,6 +367,67 @@ function parseTournamentText(text) {
return idx >= 0 ? normLines[idx] : null;
};
// Neue Funktion: Teilnahmegebühren pro Spielklasse extrahieren
const extractEntryFees = () => {
const entryFees = {};
// Verschiedene Patterns für Teilnahmegebühren suchen
const feePatterns = [
// Pattern 1: "Startgeld: U12: 5€, U14: 7€, U16: 10€"
/startgeld\s*:?\s*(.+)/i,
// Pattern 2: "Teilnahmegebühr: U12: 5€, U14: 7€"
/teilnahmegebühr\s*:?\s*(.+)/i,
// Pattern 3: "Gebühr: U12: 5€, U14: 7€"
/gebühr\s*:?\s*(.+)/i,
// Pattern 4: "Einschreibegebühr: U12: 5€, U14: 7€"
/einschreibegebühr\s*:?\s*(.+)/i,
// Pattern 5: "Anmeldegebühr: U12: 5€, U14: 7€"
/anmeldegebühr\s*:?\s*(.+)/i
];
for (const pattern of feePatterns) {
for (let i = 0; i < normLines.length; i++) {
const line = normLines[i];
const match = line.match(pattern);
if (match) {
const feeText = match[1];
// Extrahiere Gebühren aus dem Text
// Unterstützt verschiedene Formate:
// "U12: 5€, U14: 7€, U16: 10€"
// "U12: 5 Euro, U14: 7 Euro"
// "U12 5€, U14 7€"
// "U12: 5,00€, U14: 7,00€"
const feeMatches = feeText.matchAll(/(U\d+|AK\s*\d+)\s*:?\s*(\d+(?:[,.]\d+)?)\s*(?:€|Euro|EUR)?/gi);
for (const feeMatch of feeMatches) {
const ageClass = feeMatch[1].toUpperCase().replace(/\s+/g, '');
const amount = feeMatch[2].replace(',', '.');
const numericAmount = parseFloat(amount);
if (!isNaN(numericAmount)) {
entryFees[ageClass] = {
amount: numericAmount,
currency: '€',
rawText: feeMatch[0]
};
}
}
// Wenn wir Gebühren gefunden haben, brechen wir ab
if (Object.keys(entryFees).length > 0) {
break;
}
}
}
if (Object.keys(entryFees).length > 0) {
break;
}
}
return entryFees;
};
const extractBlockAfter = (labels, multiline = false) => {
const idx = normLines.findIndex(l => labels.some(lb => l.toLowerCase().startsWith(lb)));
if (idx === -1) return multiline ? [] : null;
@@ -467,7 +530,26 @@ function parseTournamentText(text) {
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;
else if (key === 'startgeld') {
comp.startgeld = val;
// Versuche auch spezifische Gebühren für diese Altersklasse zu extrahieren
const ageClassMatch = comp.altersklasseWettbewerb?.match(/(U\d+|AK\s*\d+)/i);
if (ageClassMatch) {
const ageClass = ageClassMatch[1].toUpperCase().replace(/\s+/g, '');
const feeMatch = val.match(/(\d+(?:[,.]\d+)?)\s*(?:€|Euro|EUR)?/);
if (feeMatch) {
const amount = feeMatch[1].replace(',', '.');
const numericAmount = parseFloat(amount);
if (!isNaN(numericAmount)) {
comp.entryFeeDetails = {
amount: numericAmount,
currency: '€',
ageClass: ageClass
};
}
}
}
}
}
i++;
}
@@ -515,6 +597,9 @@ function parseTournamentText(text) {
}
}
// Extrahiere Teilnahmegebühren
const entryFees = extractEntryFees();
return {
title,
termin,
@@ -526,6 +611,7 @@ function parseTournamentText(text) {
startzeiten: {},
competitions,
entries,
entryFees, // Neue: Teilnahmegebühren pro Spielklasse
debug: { normLines },
};
}

View File

@@ -11,6 +11,7 @@ const OfficialTournament = sequelize.define('OfficialTournament', {
venues: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
competitionTypes: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
registrationDeadlines: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
entryFees: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Object) - Teilnahmegebühren pro Spielklasse
}, {
tableName: 'official_tournaments',
timestamps: true,

View File

@@ -354,13 +354,15 @@ class PDFGenerator {
this.pdf.setFont('helvetica', 'bold');
this.pdf.setFontSize(12);
this.pdf.text('Wettbewerb', this.margin, y);
this.pdf.text('Datum', this.margin + 110, y);
this.pdf.text('Startzeit', this.margin + 150, y);
this.pdf.text('Datum', this.margin + 80, y);
this.pdf.text('Startzeit', this.margin + 120, y);
this.pdf.text('Gebühr', this.margin + 160, y);
y += 7;
for (const r of recommendedRows) {
this.pdf.text(r.name || '', this.margin, y);
this.pdf.text(r.date || '', this.margin + 110, y);
this.pdf.text(r.time || '', this.margin + 150, y);
this.pdf.text(r.date || '', this.margin + 80, y);
this.pdf.text(r.time || '', this.margin + 120, y);
this.pdf.text(r.entryFee || '', this.margin + 160, y);
y += 7;
if (y > this.pageHeight) {
this.addNewPage();
@@ -381,8 +383,9 @@ class PDFGenerator {
this.pdf.setFontSize(12);
for (const r of otherRows) {
this.pdf.text(r.name || '', this.margin, y);
this.pdf.text(r.date || '', this.margin + 110, y);
this.pdf.text(r.time || '', this.margin + 150, y);
this.pdf.text(r.date || '', this.margin + 80, y);
this.pdf.text(r.time || '', this.margin + 120, y);
this.pdf.text(r.entryFee || '', this.margin + 160, y);
y += 7;
if (y > this.pageHeight) {
this.addNewPage();

View File

@@ -43,6 +43,15 @@
{{ ak }}: {{ arr.join(', ') }}
</span>
</div>
<div v-if="parsed.parsedData.entryFees && Object.keys(parsed.parsedData.entryFees).length">
<strong>Teilnahmegebühren:</strong>
<div class="entry-fees">
<div v-for="(fee, ageClass) in parsed.parsedData.entryFees" :key="ageClass" class="fee-item">
<span class="age-class">{{ ageClass }}:</span>
<span class="fee-amount">{{ fee.amount }}{{ fee.currency }}</span>
</div>
</div>
</div>
</div>
<!-- ehemals 'Erkannte Einträge' entfernt -->
@@ -898,13 +907,40 @@ export default {
},
competitionsForMember(member) {
const comps = (this.parsed && this.parsed.parsedData && this.parsed.parsedData.competitions) ? this.parsed.parsedData.competitions : [];
const entryFees = (this.parsed && this.parsed.parsedData && this.parsed.parsedData.entryFees) ? this.parsed.parsedData.entryFees : {};
const rows = [];
for (let idx = 0; idx < comps.length; idx++) {
const c = comps[idx];
if (this.isEligibleForCompetition(member, c)) {
const title = c.ageClassCompetition || c.altersklasseWettbewerb || '';
const st = this.splitDateTime(c.startTime || c.startzeit || '');
rows.push({ key: String(idx), name: title, date: st.date, time: st.time, raw: c });
// Bestimme die Teilnahmegebühr für diese Altersklasse
let entryFee = '';
const ageClassMatch = title.match(/(U\d+|AK\s*\d+)/i);
if (ageClassMatch) {
const ageClass = ageClassMatch[1].toUpperCase().replace(/\s+/g, '');
if (entryFees[ageClass]) {
entryFee = `${entryFees[ageClass].amount}${entryFees[ageClass].currency}`;
}
}
// Fallback: Verwende startgeld aus der Konkurrenz
if (!entryFee && c.startgeld) {
const feeMatch = c.startgeld.match(/(\d+(?:[,.]\d+)?)\s*(?:€|Euro|EUR)?/);
if (feeMatch) {
entryFee = `${feeMatch[1]}`;
}
}
rows.push({
key: String(idx),
name: title,
date: st.date,
time: st.time,
entryFee: entryFee || '',
raw: c
});
}
}
return rows;
@@ -1261,6 +1297,35 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali
font-style: italic;
font-size: 0.85rem;
}
/* Entry Fees Styling */
.entry-fees {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.25rem;
}
.fee-item {
display: flex;
align-items: center;
gap: 0.25rem;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
.age-class {
font-weight: 600;
color: #495057;
}
.fee-amount {
font-weight: 700;
color: #28a745;
}
</style>