diff --git a/backend/controllers/tournamentController.js b/backend/controllers/tournamentController.js index c3887268..5b5536cb 100644 --- a/backend/controllers/tournamentController.js +++ b/backend/controllers/tournamentController.js @@ -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; diff --git a/backend/routes/tournamentRoutes.js b/backend/routes/tournamentRoutes.js index e4f78087..ee79146d 100644 --- a/backend/routes/tournamentRoutes.js +++ b/backend/routes/tournamentRoutes.js @@ -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); diff --git a/backend/services/internalTournamentStatsService.js b/backend/services/internalTournamentStatsService.js new file mode 100644 index 00000000..8b7a6cb3 --- /dev/null +++ b/backend/services/internalTournamentStatsService.js @@ -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} 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} opts.tmToMemberId – Turnier-Mitglied-ID -> Vereins-Mitglied-ID + * @param {Map} [opts.tmToName] – optional Namen + * @returns {Map} 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, +}; diff --git a/backend/services/tournamentService.js b/backend/services/tournamentService.js index 6f224335..fe4bb634 100644 --- a/backend/services/tournamentService.js +++ b/backend/services/tournamentService.js @@ -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(); diff --git a/frontend/src/components/tournament/InternalTournamentStats.vue b/frontend/src/components/tournament/InternalTournamentStats.vue new file mode 100644 index 00000000..89bbae38 --- /dev/null +++ b/frontend/src/components/tournament/InternalTournamentStats.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/frontend/src/i18n/locales/de-CH.json b/frontend/src/i18n/locales/de-CH.json index 43a1a302..1908c9e1 100644 --- a/frontend/src/i18n/locales/de-CH.json +++ b/frontend/src/i18n/locales/de-CH.json @@ -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", diff --git a/frontend/src/i18n/locales/de-extended.json b/frontend/src/i18n/locales/de-extended.json index 48dc7fbb..3dc1cd37 100644 --- a/frontend/src/i18n/locales/de-extended.json +++ b/frontend/src/i18n/locales/de-extended.json @@ -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", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index b23099c5..165ec73c 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -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", diff --git a/frontend/src/i18n/locales/en-AU.json b/frontend/src/i18n/locales/en-AU.json index 7dcd1c83..42f267c4 100644 --- a/frontend/src/i18n/locales/en-AU.json +++ b/frontend/src/i18n/locales/en-AU.json @@ -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", diff --git a/frontend/src/i18n/locales/en-GB.json b/frontend/src/i18n/locales/en-GB.json index d9d0c211..a80b886a 100644 --- a/frontend/src/i18n/locales/en-GB.json +++ b/frontend/src/i18n/locales/en-GB.json @@ -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", diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index bc536cac..126caf33 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -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", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index b7ebed47..efd48e39 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -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", diff --git a/frontend/src/i18n/locales/fil.json b/frontend/src/i18n/locales/fil.json index 161fa4c8..d68f936a 100644 --- a/frontend/src/i18n/locales/fil.json +++ b/frontend/src/i18n/locales/fil.json @@ -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", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 8a81172c..277d3cc0 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -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", diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index dedba998..fad055e9 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -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", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 7052a04c..069996ee 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -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", diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index 2b35aee9..aa54fa34 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -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", diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index 666b5e3e..04e2a76c 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -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", diff --git a/frontend/src/i18n/locales/tl.json b/frontend/src/i18n/locales/tl.json index 58b09843..c3c03410 100644 --- a/frontend/src/i18n/locales/tl.json +++ b/frontend/src/i18n/locales/tl.json @@ -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", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index ebc07e2f..880bc887 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -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", diff --git a/frontend/src/views/TournamentsView.vue b/frontend/src/views/TournamentsView.vue index 5b77b3b0..a7792aaa 100644 --- a/frontend/src/views/TournamentsView.vue +++ b/frontend/src/views/TournamentsView.vue @@ -30,6 +30,8 @@ {{ currentModeDescription }} + +
import TournamentTab from './TournamentTab.vue'; +import InternalTournamentStats from '../components/tournament/InternalTournamentStats.vue'; export default { name: 'TournamentsView', components: { TournamentTab, + InternalTournamentStats, }, data() { return {