feat(TournamentStats): add internal tournament statistics endpoint and localization updates
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s
- Implemented a new endpoint `getInternalTournamentStats` in the tournament controller to retrieve statistics for internal tournaments based on club ID and selected months. - Enhanced the tournament service to compute player statistics, including absolute and average rankings. - Updated tournament routes to include the new statistics endpoint. - Added localization strings for internal tournament statistics in multiple languages, improving user accessibility and experience. - Integrated the new statistics component into the TournamentsView for better user interaction.
This commit is contained in:
@@ -59,6 +59,20 @@ export const getTournaments = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** Ranglisten interne Einzel-Turniere (Gruppen- + K.-o.-Punkte) */
|
||||
export const getInternalTournamentStats = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const months = req.query.months;
|
||||
try {
|
||||
const data = await tournamentService.getInternalTournamentPlayerStats(token, clubId, months);
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Neues Turnier anlegen
|
||||
export const addTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
updateExternalParticipantSeeded,
|
||||
setExternalParticipantGaveUp,
|
||||
getTournamentClasses,
|
||||
getInternalTournamentStats,
|
||||
addTournamentClass,
|
||||
updateTournamentClass,
|
||||
deleteTournamentClass,
|
||||
@@ -55,6 +56,8 @@ import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/internal-stats/:clubId', authenticate, getInternalTournamentStats);
|
||||
|
||||
router.post('/participant', authenticate, addParticipant);
|
||||
router.post('/participants', authenticate, getParticipants);
|
||||
router.delete('/participant', authenticate, removeParticipant);
|
||||
|
||||
210
backend/services/internalTournamentStatsService.js
Normal file
210
backend/services/internalTournamentStatsService.js
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Statistik-Punkte für interne Einzel-Turniere (Gruppenphase + K.-o.-Runde).
|
||||
*
|
||||
* Gruppe: Letzter in der Gruppe = 1 Punkt, Vorletzter = 2, … Gleiche Platzierung = gleiche Punkte.
|
||||
* K.-o.: Wer die K.-o.-Runde erreicht: höchste Gruppenpunkte dieser Klasse + 1,
|
||||
* danach +1 pro gewonnenes K.-o.-Spiel.
|
||||
*/
|
||||
|
||||
export function parseWinnerFromMatch(match) {
|
||||
if (!match || !match.isFinished) return { winnerId: null, loserId: null };
|
||||
if (String(match.result || '').toUpperCase() === 'BYE') {
|
||||
const wid = match.player1Id || match.player2Id;
|
||||
const lid = wid === match.player1Id ? match.player2Id : match.player1Id;
|
||||
return { winnerId: wid || null, loserId: lid || null };
|
||||
}
|
||||
const results = match.tournamentResults;
|
||||
if (Array.isArray(results) && results.length > 0) {
|
||||
let w1 = 0;
|
||||
let w2 = 0;
|
||||
for (const r of results) {
|
||||
const p1 = Number(r.pointsPlayer1);
|
||||
const p2 = Number(r.pointsPlayer2);
|
||||
if (Number.isFinite(p1) && Number.isFinite(p2)) {
|
||||
if (p1 > p2) w1 += 1;
|
||||
else if (p2 > p1) w2 += 1;
|
||||
}
|
||||
}
|
||||
if (w1 !== w2) {
|
||||
return w1 > w2
|
||||
? { winnerId: match.player1Id, loserId: match.player2Id }
|
||||
: { winnerId: match.player2Id, loserId: match.player1Id };
|
||||
}
|
||||
}
|
||||
if (typeof match.result === 'string') {
|
||||
const tokens = match.result.match(/-?\d+\s*:\s*-?\d+/g);
|
||||
if (tokens && tokens.length > 0) {
|
||||
const last = tokens[tokens.length - 1];
|
||||
const parts = last.split(':');
|
||||
const a = Number((parts[0] || '').trim());
|
||||
const b = Number((parts[1] || '').trim());
|
||||
if (Number.isFinite(a) && Number.isFinite(b) && a !== b) {
|
||||
return a > b
|
||||
? { winnerId: match.player1Id, loserId: match.player2Id }
|
||||
: { winnerId: match.player2Id, loserId: match.player1Id };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { winnerId: null, loserId: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<{ position: number, id: number }>} rankings – Platz 1 = bestes Ergebnis; id = Turnier-Mitglied-ID
|
||||
* @returns {Map<number, number>} tournamentMemberId -> Punkte (von unten gezählt)
|
||||
*/
|
||||
export function groupPointsFromRankings(rankings) {
|
||||
const map = new Map();
|
||||
if (!rankings || rankings.length === 0) return map;
|
||||
const sorted = [...rankings].sort((a, b) => Number(b.position) - Number(a.position));
|
||||
let pts = 1;
|
||||
let i = 0;
|
||||
while (i < sorted.length) {
|
||||
const pos = Number(sorted[i].position);
|
||||
let j = i;
|
||||
while (j < sorted.length && Number(sorted[j].position) === pos) {
|
||||
const id = sorted[j].id;
|
||||
if (id != null) map.set(Number(id), pts);
|
||||
j += 1;
|
||||
}
|
||||
i = j;
|
||||
pts += 1;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {Array} opts.groups – Aus getGroupsWithParticipants (participants: flache Objekte mit id, position, isExternal, …)
|
||||
* @param {Array} opts.matches – Aus getTournamentMatches
|
||||
* @param {Array<{ id: number, isDoubles?: boolean }>} opts.classes
|
||||
* @param {Map<number, number>} opts.tmToMemberId – Turnier-Mitglied-ID -> Vereins-Mitglied-ID
|
||||
* @param {Map<number, { firstName: string, lastName: string }>} [opts.tmToName] – optional Namen
|
||||
* @returns {Map<number, { points: number, firstName: string, lastName: string }>} Vereins-Mitglied-ID -> Aggregation
|
||||
*/
|
||||
export function computeInternalSinglesStatsForTournament({
|
||||
groups,
|
||||
matches,
|
||||
classes,
|
||||
tmToMemberId,
|
||||
tmToName,
|
||||
}) {
|
||||
const doublesClassIds = new Set(
|
||||
(classes || []).filter((c) => c.isDoubles).map((c) => Number(c.id))
|
||||
);
|
||||
|
||||
const classKeys = new Set();
|
||||
for (const g of groups || []) {
|
||||
const cid = g.classId != null ? Number(g.classId) : null;
|
||||
if (cid != null && doublesClassIds.has(cid)) continue;
|
||||
classKeys.add(cid != null ? String(cid) : 'null');
|
||||
}
|
||||
for (const m of matches || []) {
|
||||
if (m.round === 'group') continue;
|
||||
const cid = m.classId != null ? Number(m.classId) : null;
|
||||
if (cid != null && doublesClassIds.has(cid)) continue;
|
||||
classKeys.add(cid != null ? String(cid) : 'null');
|
||||
}
|
||||
|
||||
const memberTotals = new Map();
|
||||
|
||||
const ensureMember = (memberId, firstName, lastName) => {
|
||||
if (memberId == null || !Number.isFinite(memberId)) return null;
|
||||
if (!memberTotals.has(memberId)) {
|
||||
memberTotals.set(memberId, {
|
||||
points: 0,
|
||||
firstName: firstName || '',
|
||||
lastName: lastName || '',
|
||||
});
|
||||
}
|
||||
return memberTotals.get(memberId);
|
||||
};
|
||||
|
||||
const nameForTm = (tmId) => {
|
||||
const n = tmToName?.get(Number(tmId));
|
||||
return { firstName: n?.firstName || '', lastName: n?.lastName || '' };
|
||||
};
|
||||
|
||||
for (const classKey of classKeys) {
|
||||
const classIdNum = classKey === 'null' ? null : Number(classKey);
|
||||
if (classIdNum != null && doublesClassIds.has(classIdNum)) continue;
|
||||
|
||||
const classGroups = (groups || []).filter((g) => {
|
||||
const gc = g.classId != null ? Number(g.classId) : null;
|
||||
const key = gc != null ? String(gc) : 'null';
|
||||
return key === classKey;
|
||||
});
|
||||
|
||||
const groupPointsByTm = new Map();
|
||||
let maxGroupPoints = 0;
|
||||
|
||||
for (const g of classGroups) {
|
||||
const parts = g.participants || [];
|
||||
if (parts.length === 0) continue;
|
||||
|
||||
const singlesRankings = parts
|
||||
.filter((p) => !p.isExternal && p.id && tmToMemberId.has(Number(p.id)))
|
||||
.map((p) => ({
|
||||
id: Number(p.id),
|
||||
position: Number(p.position || 0),
|
||||
}))
|
||||
.filter((r) => r.position > 0);
|
||||
|
||||
const m = groupPointsFromRankings(singlesRankings);
|
||||
for (const [tmId, pts] of m) {
|
||||
groupPointsByTm.set(tmId, pts);
|
||||
if (pts > maxGroupPoints) maxGroupPoints = pts;
|
||||
}
|
||||
}
|
||||
|
||||
const koMatches = (matches || []).filter((m) => {
|
||||
if (m.round === 'group') return false;
|
||||
const mid = m.classId != null ? Number(m.classId) : null;
|
||||
if (classIdNum == null) return mid == null;
|
||||
return mid === classIdNum;
|
||||
});
|
||||
|
||||
const koWins = new Map();
|
||||
const playedKo = new Set();
|
||||
|
||||
for (const m of koMatches) {
|
||||
if (!m.isFinished) continue;
|
||||
const p1 = m.player1Id;
|
||||
const p2 = m.player2Id;
|
||||
if (p1) playedKo.add(p1);
|
||||
if (p2) playedKo.add(p2);
|
||||
const { winnerId } = parseWinnerFromMatch(m);
|
||||
if (winnerId) {
|
||||
koWins.set(winnerId, (koWins.get(winnerId) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const tmIdsInClass = new Set([...groupPointsByTm.keys(), ...playedKo]);
|
||||
|
||||
for (const tmId of tmIdsInClass) {
|
||||
const mid = tmToMemberId.get(Number(tmId));
|
||||
if (mid == null) continue;
|
||||
|
||||
const gPts = groupPointsByTm.get(tmId) ?? 0;
|
||||
const wins = koWins.get(tmId) || 0;
|
||||
const inKo = playedKo.has(tmId);
|
||||
let total;
|
||||
if (inKo) {
|
||||
total = maxGroupPoints + 1 + wins;
|
||||
} else {
|
||||
total = gPts;
|
||||
}
|
||||
|
||||
const { firstName, lastName } = nameForTm(tmId);
|
||||
const row = ensureMember(mid, firstName, lastName);
|
||||
if (row) row.points += total;
|
||||
}
|
||||
}
|
||||
|
||||
return memberTotals;
|
||||
}
|
||||
|
||||
export default {
|
||||
groupPointsFromRankings,
|
||||
computeInternalSinglesStatsForTournament,
|
||||
parseWinnerFromMatch,
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { Op, literal } from 'sequelize';
|
||||
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
import { computeInternalSinglesStatsForTournament } from './internalTournamentStatsService.js';
|
||||
|
||||
function normalizeJsonConfig(value, label = 'config') {
|
||||
if (value == null) return {};
|
||||
@@ -4279,6 +4280,118 @@ Ve // 2. Neues Turnier anlegen
|
||||
await pairing.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranglisten für interne Einzel-Turniere (Punkte nach Gruppenplatz + K.-o.) über einen Zeitraum.
|
||||
*/
|
||||
async getInternalTournamentPlayerStats(userToken, clubId, months = 12) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const m = Number(months);
|
||||
const monthsNum = [3, 6, 12].includes(m) ? m : 12;
|
||||
const from = new Date();
|
||||
from.setMonth(from.getMonth() - monthsNum);
|
||||
const fromStr = from.toISOString().slice(0, 10);
|
||||
|
||||
const tournaments = await Tournament.findAll({
|
||||
where: {
|
||||
clubId: +clubId,
|
||||
allowsExternal: false,
|
||||
miniChampionshipYear: { [Op.is]: null },
|
||||
date: { [Op.gte]: fromStr },
|
||||
},
|
||||
attributes: ['id', 'name', 'date'],
|
||||
order: [['date', 'DESC']],
|
||||
});
|
||||
const list = JSON.parse(JSON.stringify(tournaments));
|
||||
|
||||
const memberAgg = new Map();
|
||||
|
||||
for (const t of list) {
|
||||
const classes = await this.getTournamentClasses(userToken, clubId, t.id);
|
||||
const classesJson = classes.map((c) => (c.toJSON ? c.toJSON() : c));
|
||||
const groups = await this.getGroupsWithParticipants(userToken, clubId, t.id);
|
||||
const matches = await this.getTournamentMatches(userToken, clubId, t.id);
|
||||
|
||||
const tmIds = new Set();
|
||||
for (const g of groups || []) {
|
||||
for (const p of g.participants || []) {
|
||||
if (p.id && !p.isExternal) tmIds.add(p.id);
|
||||
}
|
||||
}
|
||||
for (const ma of matches || []) {
|
||||
if (ma.player1Id) tmIds.add(ma.player1Id);
|
||||
if (ma.player2Id) tmIds.add(ma.player2Id);
|
||||
}
|
||||
const tmList = [...tmIds];
|
||||
const tmToMemberId = new Map();
|
||||
const tmToName = new Map();
|
||||
if (tmList.length > 0) {
|
||||
const tms = await TournamentMember.findAll({
|
||||
where: { tournamentId: t.id, id: { [Op.in]: tmList } },
|
||||
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }],
|
||||
});
|
||||
for (const row of tms) {
|
||||
const plain = row.toJSON();
|
||||
if (plain.member?.id) {
|
||||
tmToMemberId.set(plain.id, plain.member.id);
|
||||
tmToName.set(plain.id, {
|
||||
firstName: plain.member.firstName || '',
|
||||
lastName: plain.member.lastName || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const memberTotals = computeInternalSinglesStatsForTournament({
|
||||
groups,
|
||||
matches,
|
||||
classes: classesJson,
|
||||
tmToMemberId,
|
||||
tmToName,
|
||||
});
|
||||
|
||||
for (const [mid, row] of memberTotals) {
|
||||
const ex = memberAgg.get(mid) || {
|
||||
memberId: mid,
|
||||
totalPoints: 0,
|
||||
tournamentCount: 0,
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
};
|
||||
ex.totalPoints += row.points;
|
||||
ex.tournamentCount += 1;
|
||||
if (row.firstName) ex.firstName = row.firstName;
|
||||
if (row.lastName) ex.lastName = row.lastName;
|
||||
memberAgg.set(mid, ex);
|
||||
}
|
||||
}
|
||||
|
||||
const rows = [...memberAgg.values()].map((r) => ({
|
||||
memberId: r.memberId,
|
||||
firstName: r.firstName,
|
||||
lastName: r.lastName,
|
||||
totalPoints: r.totalPoints,
|
||||
tournamentCount: r.tournamentCount,
|
||||
averagePoints: r.tournamentCount > 0
|
||||
? Math.round((r.totalPoints / r.tournamentCount) * 1000) / 1000
|
||||
: 0,
|
||||
}));
|
||||
|
||||
const absoluteRanking = [...rows].sort((a, b) =>
|
||||
b.totalPoints - a.totalPoints || (a.lastName || '').localeCompare(b.lastName || '', 'de')
|
||||
);
|
||||
const averageRanking = [...rows].sort((a, b) =>
|
||||
b.averagePoints - a.averagePoints || (a.lastName || '').localeCompare(b.lastName || '', 'de')
|
||||
);
|
||||
|
||||
return {
|
||||
months: monthsNum,
|
||||
fromDate: fromStr,
|
||||
tournamentCount: list.length,
|
||||
absoluteRanking,
|
||||
averageRanking,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new TournamentService();
|
||||
|
||||
225
frontend/src/components/tournament/InternalTournamentStats.vue
Normal file
225
frontend/src/components/tournament/InternalTournamentStats.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<section class="internal-tournament-stats" v-if="clubId">
|
||||
<div class="stats-header">
|
||||
<h3>{{ $t('tournaments.internalStatsTitle') }}</h3>
|
||||
<label class="period-label">
|
||||
<span>{{ $t('tournaments.internalStatsPeriod') }}</span>
|
||||
<select v-model="months" @change="load" :disabled="loading">
|
||||
<option :value="12">{{ $t('tournaments.internalStatsLast12Months') }}</option>
|
||||
<option :value="6">{{ $t('tournaments.internalStatsLast6Months') }}</option>
|
||||
<option :value="3">{{ $t('tournaments.internalStatsLast3Months') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p class="stats-meta">
|
||||
{{ $t('tournaments.internalStatsTournamentsInPeriod', { count: stats.tournamentCount || 0 }) }}
|
||||
</p>
|
||||
<p class="stats-explain">{{ $t('tournaments.internalStatsPointsExplain') }}</p>
|
||||
|
||||
<div v-if="loading" class="stats-loading">{{ $t('common.loading') }}</div>
|
||||
<div v-else-if="error" class="stats-error">{{ error }}</div>
|
||||
<div v-else class="stats-grid">
|
||||
<div class="stats-column">
|
||||
<h4>{{ $t('tournaments.internalStatsAbsoluteRank') }}</h4>
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-rank">#</th>
|
||||
<th>{{ $t('tournaments.player') }}</th>
|
||||
<th>{{ $t('tournaments.internalStatsPoints') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in stats.absoluteRanking" :key="'a-' + row.memberId">
|
||||
<td class="col-rank">{{ i + 1 }}</td>
|
||||
<td>{{ row.firstName }} {{ row.lastName }}</td>
|
||||
<td>{{ row.totalPoints }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="!stats.absoluteRanking?.length" class="stats-empty">{{ $t('tournaments.internalStatsEmpty') }}</p>
|
||||
</div>
|
||||
<div class="stats-column">
|
||||
<h4>{{ $t('tournaments.internalStatsAverageRank') }}</h4>
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-rank">#</th>
|
||||
<th>{{ $t('tournaments.player') }}</th>
|
||||
<th>{{ $t('tournaments.internalStatsAvgPoints') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in stats.averageRanking" :key="'v-' + row.memberId">
|
||||
<td class="col-rank">{{ i + 1 }}</td>
|
||||
<td>{{ row.firstName }} {{ row.lastName }}</td>
|
||||
<td>{{ formatAvg(row.averagePoints) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="!stats.averageRanking?.length" class="stats-empty">{{ $t('tournaments.internalStatsEmpty') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import apiClient from '../../apiClient.js';
|
||||
|
||||
export default {
|
||||
name: 'InternalTournamentStats',
|
||||
data() {
|
||||
return {
|
||||
months: 12,
|
||||
loading: false,
|
||||
error: null,
|
||||
stats: {
|
||||
tournamentCount: 0,
|
||||
absoluteRanking: [],
|
||||
averageRanking: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
clubId: (state) => state.currentClub,
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
clubId() {
|
||||
this.load();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
formatAvg(v) {
|
||||
if (v == null || Number.isNaN(v)) return '–';
|
||||
return String(v);
|
||||
},
|
||||
async load() {
|
||||
if (!this.clubId) return;
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const res = await apiClient.get(`/tournament/internal-stats/${this.clubId}`, {
|
||||
params: { months: this.months },
|
||||
});
|
||||
this.stats = res.data || {};
|
||||
} catch (e) {
|
||||
this.error = e?.response?.data?.error || e.message || 'Error';
|
||||
this.stats = { tournamentCount: 0, absoluteRanking: [], averageRanking: [] };
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.internal-tournament-stats {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
border-radius: var(--border-radius, 8px);
|
||||
background: var(--background-light, #fafafa);
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stats-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.period-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.period-label select {
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color, #d1d5db);
|
||||
}
|
||||
|
||||
.stats-meta {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted, #6b7280);
|
||||
}
|
||||
|
||||
.stats-explain {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: #4b5563;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-column h4 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.stats-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-table th,
|
||||
.stats-table td {
|
||||
padding: 0.45rem 0.6rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.stats-table th {
|
||||
background: #f9fafb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.col-rank {
|
||||
width: 2.5rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stats-loading,
|
||||
.stats-error,
|
||||
.stats-empty {
|
||||
font-size: 0.9rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stats-error {
|
||||
color: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
@@ -175,6 +175,18 @@
|
||||
"cancel": "Abbreche"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
|
||||
"internalStatsPeriod": "Ziitruum",
|
||||
"internalStatsLast12Months": "Letscht 12 Mönet",
|
||||
"internalStatsLast6Months": "Letscht 6 Mönet",
|
||||
"internalStatsLast3Months": "Letscht 3 Mönet",
|
||||
"internalStatsTournamentsInPeriod": "{count} Turnier(e) im Ziitruum (ohni Minimeisterschafte).",
|
||||
"internalStatsPointsExplain": "Pünkt: I jede Gruppe chunnt dr Letscht 1 Punkt, dr Vorletzt 2, usw.; gliichi Platzierig = gliichi Pünkt. Wer d K.-o.-Rundi erreicht, bechunnt di höchschte Gruppepünkt vo de Chlass plus 1, deno no 1 Punkt pro gwunne K.-o.-Spiel. Nur Veräinsmitglieder (Einzel).",
|
||||
"internalStatsAbsoluteRank": "Rangliste Gsamtpünkt",
|
||||
"internalStatsAverageRank": "Rangliste Durchschnitt (Pünkt pro Turnier)",
|
||||
"internalStatsPoints": "Pünkt",
|
||||
"internalStatsAvgPoints": "Ø",
|
||||
"internalStatsEmpty": "Kei uswertbari Date im gwählte Ziitruum.",
|
||||
"numberOfTables": "Aazahl Tisch",
|
||||
"table": "Tisch",
|
||||
"playerOne": "Spieler 1",
|
||||
|
||||
@@ -406,6 +406,18 @@
|
||||
}
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
|
||||
"internalStatsPeriod": "Zeitraum",
|
||||
"internalStatsLast12Months": "Letzte 12 Monate",
|
||||
"internalStatsLast6Months": "Letzte 6 Monate",
|
||||
"internalStatsLast3Months": "Letzte 3 Monate",
|
||||
"internalStatsTournamentsInPeriod": "{count} Turnier(e) im Zeitraum (ohne Minimeisterschaften).",
|
||||
"internalStatsPointsExplain": "Punkte: In jeder Gruppe erhält der Letzte 1 Punkt, der Vorletzte 2, usw.; gleiche Platzierung = gleiche Punkte. Wer die K.-o.-Runde erreicht, erhält die höchsten Gruppenpunkte der Klasse plus 1, danach je weiteren gewonnenen K.-o.-Spiel einen Punkt. Nur Vereinsmitglieder (Einzel).",
|
||||
"internalStatsAbsoluteRank": "Rangliste Gesamtpunkte",
|
||||
"internalStatsAverageRank": "Rangliste Durchschnitt (Punkte pro Turnier)",
|
||||
"internalStatsPoints": "Punkte",
|
||||
"internalStatsAvgPoints": "Ø",
|
||||
"internalStatsEmpty": "Keine auswertbaren Daten im gewählten Zeitraum.",
|
||||
"title": "Turniere",
|
||||
"tournamentName": "Turniername",
|
||||
"events": "Veranstaltungen",
|
||||
|
||||
@@ -698,6 +698,18 @@
|
||||
},
|
||||
"tournaments": {
|
||||
"internalTournaments": "Interne Turniere",
|
||||
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
|
||||
"internalStatsPeriod": "Zeitraum",
|
||||
"internalStatsLast12Months": "Letzte 12 Monate",
|
||||
"internalStatsLast6Months": "Letzte 6 Monate",
|
||||
"internalStatsLast3Months": "Letzte 3 Monate",
|
||||
"internalStatsTournamentsInPeriod": "{count} Turnier(e) im Zeitraum (ohne Minimeisterschaften).",
|
||||
"internalStatsPointsExplain": "Punkte: In jeder Gruppe erhält der Letzte 1 Punkt, der Vorletzte 2, usw.; gleiche Platzierung = gleiche Punkte. Wer die K.-o.-Runde erreicht, erhält die höchsten Gruppenpunkte der Klasse plus 1, danach je weiteren gewonnenen K.-o.-Spiel einen Punkt. Nur Vereinsmitglieder (Einzel).",
|
||||
"internalStatsAbsoluteRank": "Rangliste Gesamtpunkte",
|
||||
"internalStatsAverageRank": "Rangliste Durchschnitt (Punkte pro Turnier)",
|
||||
"internalStatsPoints": "Punkte",
|
||||
"internalStatsAvgPoints": "Ø",
|
||||
"internalStatsEmpty": "Keine auswertbaren Daten im gewählten Zeitraum.",
|
||||
"openTournaments": "Offene Turniere",
|
||||
"miniChampionships": "Minimeisterschaften",
|
||||
"newMiniChampionship": "Neue Minimeisterschaft",
|
||||
|
||||
@@ -175,6 +175,18 @@
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Internal tournaments statistics (singles)",
|
||||
"internalStatsPeriod": "Period",
|
||||
"internalStatsLast12Months": "Last 12 months",
|
||||
"internalStatsLast6Months": "Last 6 months",
|
||||
"internalStatsLast3Months": "Last 3 months",
|
||||
"internalStatsTournamentsInPeriod": "{count} tournament(s) in this period (excluding mini championships).",
|
||||
"internalStatsPointsExplain": "Points: In each group, last place scores 1, second-to-last 2, and so on; tied positions share the same points. Players who reach the knockout stage get the highest group points in that class plus 1, then one point per knockout match won. Club members in singles classes only.",
|
||||
"internalStatsAbsoluteRank": "Total points ranking",
|
||||
"internalStatsAverageRank": "Average points per tournament",
|
||||
"internalStatsPoints": "Points",
|
||||
"internalStatsAvgPoints": "Avg.",
|
||||
"internalStatsEmpty": "No data for the selected period.",
|
||||
"numberOfTables": "Number of tables",
|
||||
"table": "Table",
|
||||
"playerOne": "Player 1",
|
||||
|
||||
@@ -356,6 +356,18 @@
|
||||
"selectGroup": "Select group..."
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Internal tournaments statistics (singles)",
|
||||
"internalStatsPeriod": "Period",
|
||||
"internalStatsLast12Months": "Last 12 months",
|
||||
"internalStatsLast6Months": "Last 6 months",
|
||||
"internalStatsLast3Months": "Last 3 months",
|
||||
"internalStatsTournamentsInPeriod": "{count} tournament(s) in this period (excluding mini championships).",
|
||||
"internalStatsPointsExplain": "Points: In each group, last place scores 1, second-to-last 2, and so on; tied positions share the same points. Players who reach the knockout stage get the highest group points in that class plus 1, then one point per knockout match won. Club members in singles classes only.",
|
||||
"internalStatsAbsoluteRank": "Total points ranking",
|
||||
"internalStatsAverageRank": "Average points per tournament",
|
||||
"internalStatsPoints": "Points",
|
||||
"internalStatsAvgPoints": "Avg.",
|
||||
"internalStatsEmpty": "No data for the selected period.",
|
||||
"numberOfTables": "Number of tables",
|
||||
"table": "Table",
|
||||
"playerOne": "Player 1",
|
||||
|
||||
@@ -175,6 +175,18 @@
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Internal tournaments statistics (singles)",
|
||||
"internalStatsPeriod": "Period",
|
||||
"internalStatsLast12Months": "Last 12 months",
|
||||
"internalStatsLast6Months": "Last 6 months",
|
||||
"internalStatsLast3Months": "Last 3 months",
|
||||
"internalStatsTournamentsInPeriod": "{count} tournament(s) in this period (excluding mini championships).",
|
||||
"internalStatsPointsExplain": "Points: In each group, last place scores 1, second-to-last 2, and so on; tied positions share the same points. Players who reach the knockout stage get the highest group points in that class plus 1, then one point per knockout match won. Club members in singles classes only.",
|
||||
"internalStatsAbsoluteRank": "Total points ranking",
|
||||
"internalStatsAverageRank": "Average points per tournament",
|
||||
"internalStatsPoints": "Points",
|
||||
"internalStatsAvgPoints": "Avg.",
|
||||
"internalStatsEmpty": "No data for the selected period.",
|
||||
"numberOfTables": "Number of tables",
|
||||
"table": "Table",
|
||||
"playerOne": "Player 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Estadísticas de torneos internos (individual)",
|
||||
"internalStatsPeriod": "Periodo",
|
||||
"internalStatsLast12Months": "Últimos 12 meses",
|
||||
"internalStatsLast6Months": "Últimos 6 meses",
|
||||
"internalStatsLast3Months": "Últimos 3 meses",
|
||||
"internalStatsTournamentsInPeriod": "{count} torneo(s) en el periodo (sin minicampeonatos).",
|
||||
"internalStatsPointsExplain": "Puntos: en cada grupo, el último suma 1, el penúltimo 2, etc.; empate en la clasificación = mismos puntos. Quien llega al KO recibe los puntos de grupo más altos de la clase más 1, y después 1 punto por partido de KO ganado. Solo socios en individual.",
|
||||
"internalStatsAbsoluteRank": "Clasificación por puntos totales",
|
||||
"internalStatsAverageRank": "Clasificación por media (puntos por torneo)",
|
||||
"internalStatsPoints": "Puntos",
|
||||
"internalStatsAvgPoints": "Media",
|
||||
"internalStatsEmpty": "No hay datos para el periodo seleccionado.",
|
||||
"numberOfTables": "Número de mesas",
|
||||
"table": "Mesa",
|
||||
"playerOne": "Jugador 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "Kanselahin"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Estadistika ng internal na paligsahan (singles)",
|
||||
"internalStatsPeriod": "Saklaw",
|
||||
"internalStatsLast12Months": "Huling 12 buwan",
|
||||
"internalStatsLast6Months": "Huling 6 na buwan",
|
||||
"internalStatsLast3Months": "Huling 3 buwan",
|
||||
"internalStatsTournamentsInPeriod": "{count} paligsahan sa panahon (hindi kasama ang mini championships).",
|
||||
"internalStatsPointsExplain": "Puntos: Sa bawat grupo, pinakahuli = 1, bago sa huli = 2, atbp.; parehong ranggo = parehong puntos. Ang mga pumasok sa knockout ay tumatanggap ng pinakamataas na grupo puntos ng klase plus 1, at +1 bawat panalong laro sa KO. Mga miyembro lamang, singles.",
|
||||
"internalStatsAbsoluteRank": "Ranggo sa kabuuang puntos",
|
||||
"internalStatsAverageRank": "Ranggo sa average (puntos bawat paligsahan)",
|
||||
"internalStatsPoints": "Puntos",
|
||||
"internalStatsAvgPoints": "Avg.",
|
||||
"internalStatsEmpty": "Walang datos sa napiling panahon.",
|
||||
"numberOfTables": "Bilang ng mesa",
|
||||
"table": "Mesa",
|
||||
"playerOne": "Manlalaro 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Statistiques des tournois internes (simple)",
|
||||
"internalStatsPeriod": "Période",
|
||||
"internalStatsLast12Months": "12 derniers mois",
|
||||
"internalStatsLast6Months": "6 derniers mois",
|
||||
"internalStatsLast3Months": "3 derniers mois",
|
||||
"internalStatsTournamentsInPeriod": "{count} tournoi(s) sur la période (hors mini-championnats).",
|
||||
"internalStatsPointsExplain": "Points : dans chaque poule, le dernier reçoit 1, l’avant-dernier 2, etc. ; même place = mêmes points. Les joueurs en phase KO reçoivent le meilleur total de poule de la catégorie plus 1, puis 1 point par match KO gagné. Membres du club en simple uniquement.",
|
||||
"internalStatsAbsoluteRank": "Classement des points totaux",
|
||||
"internalStatsAverageRank": "Classement par moyenne (points par tournoi)",
|
||||
"internalStatsPoints": "Points",
|
||||
"internalStatsAvgPoints": "Moy.",
|
||||
"internalStatsEmpty": "Aucune donnée pour la période choisie.",
|
||||
"numberOfTables": "Nombre de tables",
|
||||
"table": "Table",
|
||||
"playerOne": "Joueur 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "Annulla"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Statistiche tornei interni (singolo)",
|
||||
"internalStatsPeriod": "Periodo",
|
||||
"internalStatsLast12Months": "Ultimi 12 mesi",
|
||||
"internalStatsLast6Months": "Ultimi 6 mesi",
|
||||
"internalStatsLast3Months": "Ultimi 3 mesi",
|
||||
"internalStatsTournamentsInPeriod": "{count} torneo/i nel periodo (esclusi i mini-campionati).",
|
||||
"internalStatsPointsExplain": "Punti: in ogni girone l’ultimo ha 1, il penultimo 2, ecc.; stesso posto = stessi punti. Chi raggiunge il KO ottiene il massimo punti girone della classe più 1, poi 1 punto per ogni partita KO vinta. Solo soci al singolo.",
|
||||
"internalStatsAbsoluteRank": "Classifica punti totali",
|
||||
"internalStatsAverageRank": "Classifica media (punti per torneo)",
|
||||
"internalStatsPoints": "Punti",
|
||||
"internalStatsAvgPoints": "Media",
|
||||
"internalStatsEmpty": "Nessun dato nel periodo selezionato.",
|
||||
"numberOfTables": "Numero di tavoli",
|
||||
"table": "Tavolo",
|
||||
"playerOne": "Giocatore 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "内部大会の統計(シングルス)",
|
||||
"internalStatsPeriod": "期間",
|
||||
"internalStatsLast12Months": "過去12か月",
|
||||
"internalStatsLast6Months": "過去6か月",
|
||||
"internalStatsLast3Months": "過去3か月",
|
||||
"internalStatsTournamentsInPeriod": "期間内の大会 {count} 件(ミニ選手権は除く)。",
|
||||
"internalStatsPointsExplain": "得点:各グループで最下位が1点、その上が2点…同順位は同点。ノックアウトに進んだ選手はクラス最高のグループ得点に1を加え、KOで勝った試合ごとにさらに1点。クラブ会員のシングルのみ。",
|
||||
"internalStatsAbsoluteRank": "総得点ランキング",
|
||||
"internalStatsAverageRank": "平均得点ランキング(大会あたり)",
|
||||
"internalStatsPoints": "得点",
|
||||
"internalStatsAvgPoints": "平均",
|
||||
"internalStatsEmpty": "選択した期間にデータがありません。",
|
||||
"numberOfTables": "卓数",
|
||||
"table": "卓",
|
||||
"playerOne": "選手 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "Anuluj"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Statystyki turniejów wewnętrznych (singel)",
|
||||
"internalStatsPeriod": "Okres",
|
||||
"internalStatsLast12Months": "Ostatnie 12 miesięcy",
|
||||
"internalStatsLast6Months": "Ostatnie 6 miesięcy",
|
||||
"internalStatsLast3Months": "Ostatnie 3 miesiące",
|
||||
"internalStatsTournamentsInPeriod": "{count} turniej(ów) w okresie (bez mini-mistrzostw).",
|
||||
"internalStatsPointsExplain": "Punkty: w każdej grupie ostatni ma 1, przedostatni 2 itd.; ten sam ranking = te same punkty. Gracze w fazie pucharowej: maks. punkty grupowe klasy plus 1, potem 1 pkt za każdy wygrany mecz KO. Tylko członkowie klubu, gra pojedyncza.",
|
||||
"internalStatsAbsoluteRank": "Ranking sumy punktów",
|
||||
"internalStatsAverageRank": "Ranking średniej (punkty na turniej)",
|
||||
"internalStatsPoints": "Punkty",
|
||||
"internalStatsAvgPoints": "Śr.",
|
||||
"internalStatsEmpty": "Brak danych w wybranym okresie.",
|
||||
"numberOfTables": "Liczba stołów",
|
||||
"table": "Stół",
|
||||
"playerOne": "Zawodnik 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "ยกเลิก"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "สถิติการแข่งขันภายใน (เดี่ยว)",
|
||||
"internalStatsPeriod": "ช่วงเวลา",
|
||||
"internalStatsLast12Months": "12 เดือนล่าสุด",
|
||||
"internalStatsLast6Months": "6 เดือนล่าสุด",
|
||||
"internalStatsLast3Months": "3 เดือนล่าสุด",
|
||||
"internalStatsTournamentsInPeriod": "{count} การแข่งขันในช่วงเวลา (ไม่รวมมินิแชมเปียนชิป)",
|
||||
"internalStatsPointsExplain": "คะแนน: ในแต่ละกลุ่ม อันดับสุดท้ายได้ 1 รองสุดท้ายได้ 2 ต่อไปเรื่อยๆ อันดับเดียวกันได้คะแนนเท่ากัน ผู้เข้ารอบน็อกเอาต์ได้คะแนนกลุ่มสูงสุดของคลาสบวก 1 แล้วบวก 1 ต่อการชนะแมตช์ KO หนึ่งนัด เฉพาะสมาชิกสโมสร ประเภทเดี่ยว",
|
||||
"internalStatsAbsoluteRank": "อันดับคะแนนรวม",
|
||||
"internalStatsAverageRank": "อันดับค่าเฉลี่ย (คะแนนต่อการแข่งขัน)",
|
||||
"internalStatsPoints": "คะแนน",
|
||||
"internalStatsAvgPoints": "เฉลี่ย",
|
||||
"internalStatsEmpty": "ไม่มีข้อมูลในช่วงเวลาที่เลือก",
|
||||
"numberOfTables": "จำนวนโต๊ะ",
|
||||
"table": "โต๊ะ",
|
||||
"playerOne": "ผู้เล่น 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "Kanselahin"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "Istatistika ng internal na tournament (singles)",
|
||||
"internalStatsPeriod": "Saklaw",
|
||||
"internalStatsLast12Months": "Huling 12 buwan",
|
||||
"internalStatsLast6Months": "Huling 6 na buwan",
|
||||
"internalStatsLast3Months": "Huling 3 buwan",
|
||||
"internalStatsTournamentsInPeriod": "{count} tournament sa panahon (hindi kasama ang mini championships).",
|
||||
"internalStatsPointsExplain": "Puntos: Sa bawat grupo, ang huli ay 1, bago sa huli ay 2, atbp.; parehong ranggo = parehong puntos. Ang mga nakapasok sa knockout ay nakakakuha ng pinakamataas na grupo puntos ng klase plus 1, at +1 bawat panalong laro sa KO. Mga miyembro lamang sa singles.",
|
||||
"internalStatsAbsoluteRank": "Ranggo sa kabuuang puntos",
|
||||
"internalStatsAverageRank": "Ranggo sa average (puntos bawat tournament)",
|
||||
"internalStatsPoints": "Puntos",
|
||||
"internalStatsAvgPoints": "Avg.",
|
||||
"internalStatsEmpty": "Walang datos sa napiling panahon.",
|
||||
"numberOfTables": "Bilang ng mesa",
|
||||
"table": "Mesa",
|
||||
"playerOne": "Manlalaro 1",
|
||||
|
||||
@@ -174,6 +174,18 @@
|
||||
"cancel": "取消"
|
||||
},
|
||||
"tournaments": {
|
||||
"internalStatsTitle": "内部锦标赛统计(单打)",
|
||||
"internalStatsPeriod": "时间范围",
|
||||
"internalStatsLast12Months": "过去 12 个月",
|
||||
"internalStatsLast6Months": "过去 6 个月",
|
||||
"internalStatsLast3Months": "过去 3 个月",
|
||||
"internalStatsTournamentsInPeriod": "该期间共 {count} 场锦标赛(不含迷你锦标赛)。",
|
||||
"internalStatsPointsExplain": "计分:每组中最后一名得 1 分,倒数第二名得 2 分,以此类推;相同名次得分相同。进入淘汰赛的选手获得该级别最高小组赛分数加 1,之后每赢一场淘汰赛再加 1 分。仅统计俱乐部成员单打。",
|
||||
"internalStatsAbsoluteRank": "总积分榜",
|
||||
"internalStatsAverageRank": "平均分榜(每场锦标赛)",
|
||||
"internalStatsPoints": "分数",
|
||||
"internalStatsAvgPoints": "平均",
|
||||
"internalStatsEmpty": "所选期间没有可统计的数据。",
|
||||
"numberOfTables": "球台数量",
|
||||
"table": "球台",
|
||||
"playerOne": "选手 1",
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
{{ currentModeDescription }}
|
||||
</div>
|
||||
|
||||
<InternalTournamentStats v-if="activeMode === 'internal'" />
|
||||
|
||||
<div class="tab-content">
|
||||
<TournamentTab
|
||||
:key="activeMode"
|
||||
@@ -42,11 +44,13 @@
|
||||
|
||||
<script>
|
||||
import TournamentTab from './TournamentTab.vue';
|
||||
import InternalTournamentStats from '../components/tournament/InternalTournamentStats.vue';
|
||||
|
||||
export default {
|
||||
name: 'TournamentsView',
|
||||
components: {
|
||||
TournamentTab,
|
||||
InternalTournamentStats,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user