Fügt das Modell OfficialCompetitionMember hinzu und implementiert die Logik zur Verwaltung der Teilnahme von Mitgliedern an offiziellen Wettbewerben. Aktualisiert die Routen und Controller, um die Teilnahmeinformationen zu speichern und abzurufen. Ergänzt die Benutzeroberfläche in OfficialTournaments.vue zur Anzeige und Bearbeitung der Teilnahmeoptionen für Mitglieder.
This commit is contained in:
@@ -50,7 +50,7 @@
|
||||
</a>
|
||||
<a href="/tournaments" class="nav-link">
|
||||
<span class="nav-icon">🏆</span>
|
||||
Turniere
|
||||
Interne Turniere
|
||||
</a>
|
||||
<a href="/official-tournaments" class="nav-link">
|
||||
<span class="nav-icon">📄</span>
|
||||
|
||||
@@ -264,7 +264,7 @@ class PDFGenerator {
|
||||
});
|
||||
}
|
||||
|
||||
addMemberCompetitions(tournamentTitle, memberName, recommendedRows = [], otherRows = []) {
|
||||
addMemberCompetitions(tournamentTitle, memberName, recommendedRows = [], otherRows = [], venues = []) {
|
||||
let y = this.margin;
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(14);
|
||||
@@ -319,6 +319,37 @@ class PDFGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Austragungsort(e) direkt vor den Hinweisen
|
||||
const venueLines = Array.isArray(venues) ? venues.filter(Boolean) : [];
|
||||
if (venueLines.length) {
|
||||
const heading = venueLines.length === 1 ? 'Austragungsort' : 'Austragungsorte';
|
||||
const maxWidth = 210 - this.margin * 2;
|
||||
if (y + 20 + venueLines.length * 6 > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
} else {
|
||||
y += 6;
|
||||
}
|
||||
this.pdf.setFont('helvetica', 'bold');
|
||||
this.pdf.setFontSize(13);
|
||||
this.pdf.text(`${heading}:`, this.margin, y);
|
||||
y += 7;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(12);
|
||||
for (const v of venueLines) {
|
||||
const wrapped = this.pdf.splitTextToSize(String(v), maxWidth);
|
||||
for (const line of wrapped) {
|
||||
this.pdf.text(line, this.margin, y);
|
||||
y += 6;
|
||||
if (y > this.pageHeight) {
|
||||
this.addNewPage();
|
||||
y = this.margin;
|
||||
this.pdf.setFont('helvetica', 'normal');
|
||||
this.pdf.setFontSize(12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hinweise-Sektion
|
||||
const remainingForHints = 60; // Platz für Überschrift + Liste abschätzen
|
||||
if (y + remainingForHints > this.pageHeight) {
|
||||
|
||||
@@ -87,13 +87,42 @@
|
||||
<th>Name</th>
|
||||
<th>Geburtsdatum</th>
|
||||
<th>Alter</th>
|
||||
<th>Angemeldet</th>
|
||||
<th>Teilgenommen</th>
|
||||
<th>Platzierung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in eligibleMembers(c)" :key="m.id">
|
||||
<td>{{ m.firstName }} {{ m.lastName }}</td>
|
||||
<td>
|
||||
<label style="display:flex; align-items:center; gap:.4rem;">
|
||||
<input type="checkbox"
|
||||
:checked="getParticipation(c.id, m.id).wants"
|
||||
@change="onToggleWants(c, m, $event.target.checked)"
|
||||
title="Teilnahme-Wunsch" />
|
||||
<span>{{ m.firstName }} {{ m.lastName }}</span>
|
||||
</label>
|
||||
</td>
|
||||
<td>{{ formatDateStr(m.birthDate) }}</td>
|
||||
<td>{{ ageOnRef(m, c) ?? '–' }}</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
:checked="getParticipation(c.id, m.id).registered"
|
||||
@change="onToggleRegistered(c, m, $event.target.checked)"
|
||||
title="Angemeldet" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
:checked="getParticipation(c.id, m.id).participated"
|
||||
@change="onToggleParticipated(c, m, $event.target.checked)"
|
||||
title="Teilgenommen" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" style="width:7rem;"
|
||||
:value="getParticipation(c.id, m.id).placement || ''"
|
||||
@change="onPlacementChange(c, m, $event.target.value)"
|
||||
placeholder="z.B. 3. Platz" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -167,6 +196,7 @@ export default {
|
||||
showMemberDialog: false,
|
||||
memberRecommendations: {},
|
||||
selectedMemberIdForDialog: null,
|
||||
participationMap: {}, // key: `${competitionId}-${memberId}` => { wants, registered, participated, placement }
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -211,6 +241,7 @@ export default {
|
||||
if (!this.uploadedId) return;
|
||||
const t = await apiClient.get(`/official-tournaments/${this.currentClub}/${this.uploadedId}`);
|
||||
this.parsed = t.data;
|
||||
this.buildParticipationMap(t.data && t.data.participation ? t.data.participation : []);
|
||||
// Mitglieder laden (alle aktiv)
|
||||
const m = await apiClient.get(`/clubmembers/get/${this.currentClub}/true`);
|
||||
this.members = m.data;
|
||||
@@ -219,6 +250,61 @@ export default {
|
||||
const r = await apiClient.get(`/official-tournaments/${this.currentClub}`);
|
||||
this.list = r.data;
|
||||
},
|
||||
buildParticipationMap(entries) {
|
||||
const map = {};
|
||||
for (const e of entries) {
|
||||
const key = `${e.competitionId}-${e.memberId}`;
|
||||
map[key] = {
|
||||
wants: !!e.wants,
|
||||
registered: !!e.registered,
|
||||
participated: !!e.participated,
|
||||
placement: e.placement || null,
|
||||
};
|
||||
}
|
||||
this.participationMap = map;
|
||||
},
|
||||
getParticipation(competitionId, memberId) {
|
||||
const key = `${competitionId}-${memberId}`;
|
||||
if (!this.participationMap[key]) {
|
||||
this.$set ? this.$set(this.participationMap, key, { wants: false, registered: false, participated: false, placement: null })
|
||||
: (this.participationMap[key] = { wants: false, registered: false, participated: false, placement: null });
|
||||
}
|
||||
return this.participationMap[key];
|
||||
},
|
||||
async saveParticipation(competitionId, memberId) {
|
||||
const key = `${competitionId}-${memberId}`;
|
||||
const p = this.participationMap[key] || { wants: false, registered: false, participated: false, placement: null };
|
||||
try {
|
||||
await apiClient.post(`/official-tournaments/${this.currentClub}/${this.uploadedId}/participation`, {
|
||||
competitionId,
|
||||
memberId,
|
||||
wants: !!p.wants,
|
||||
registered: !!p.registered,
|
||||
participated: !!p.participated,
|
||||
placement: p.placement || null,
|
||||
});
|
||||
} catch (e) {
|
||||
// optional: Fehleranzeige
|
||||
}
|
||||
},
|
||||
onToggleWants(c, m, val) {
|
||||
const key = `${c.id}-${m.id}`;
|
||||
this.getParticipation(c.id, m.id).wants = !!val;
|
||||
this.saveParticipation(c.id, m.id);
|
||||
},
|
||||
onToggleRegistered(c, m, val) {
|
||||
this.getParticipation(c.id, m.id).registered = !!val;
|
||||
this.saveParticipation(c.id, m.id);
|
||||
},
|
||||
onToggleParticipated(c, m, val) {
|
||||
this.getParticipation(c.id, m.id).participated = !!val;
|
||||
this.saveParticipation(c.id, m.id);
|
||||
},
|
||||
onPlacementChange(c, m, val) {
|
||||
const v = (val || '').trim();
|
||||
this.getParticipation(c.id, m.id).placement = v || null;
|
||||
this.saveParticipation(c.id, m.id);
|
||||
},
|
||||
// Auswahl Helfer + PDF-Generierung
|
||||
openMemberDialog() { this.showMemberDialog = true; },
|
||||
closeMemberDialog() { this.showMemberDialog = false; },
|
||||
@@ -274,7 +360,10 @@ export default {
|
||||
const otherRows = rows.filter(r => !recKeys.includes(r.key));
|
||||
if (!first) pdf.addNewPage();
|
||||
first = false;
|
||||
pdf.addMemberCompetitions(title, `${m.firstName} ${m.lastName}`, recRows, otherRows);
|
||||
const venues = (this.parsed && this.parsed.parsedData && Array.isArray(this.parsed.parsedData.austragungsorte))
|
||||
? this.parsed.parsedData.austragungsorte
|
||||
: [];
|
||||
pdf.addMemberCompetitions(title, `${m.firstName} ${m.lastName}`, recRows, otherRows, venues);
|
||||
}
|
||||
pdf.save('turnier_mitglieder.pdf');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user