feat(TournamentStats): enhance internal tournament statistics with age class filtering
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 37s

- Updated the `getInternalTournamentPlayerStats` endpoint to accept age class keys for more granular statistics.
- Introduced new utility functions for handling age class filtering in the internal tournament stats service.
- Enhanced the InternalTournamentStats component with a new age class filter UI, allowing users to select specific age classes for their statistics.
- Updated localization strings across multiple languages to support the new age class filtering feature, improving user accessibility and understanding.
This commit is contained in:
Torsten Schulz (local)
2026-04-08 12:50:20 +02:00
parent c1b8b2c665
commit 27f8186d91
19 changed files with 413 additions and 6 deletions

View File

@@ -64,8 +64,9 @@ export const getInternalTournamentStats = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId } = req.params;
const months = req.query.months;
const ageClassKeys = req.query.ageClassKeys;
try {
const data = await tournamentService.getInternalTournamentPlayerStats(token, clubId, months);
const data = await tournamentService.getInternalTournamentPlayerStats(token, clubId, months, ageClassKeys);
res.status(200).json(data);
} catch (error) {
console.error(error);

View File

@@ -81,6 +81,35 @@ export function groupPercentFromRankings(rankings, nInGroup) {
return map;
}
/** Schlüssel für Gruppen/Spiele ohne zugeordnete Klasse (classId null) */
export const NO_CLASS_AGE_KEY = 'ak|__noclass__';
/**
* Stabiler Filter-Schlüssel aus einer Einzel-Klasse (Geburtsjahre + Geschlecht).
* @param {object} classRow TournamentClass-Plain
* @returns {string|null} null bei Doppel
*/
export function ageClassFilterKey(classRow) {
if (!classRow || classRow.isDoubles) return null;
const min = classRow.minBirthYear ?? classRow.min_birth_year ?? null;
const max = classRow.maxBirthYear ?? classRow.max_birth_year ?? null;
const g = classRow.gender ?? '';
return `ak|${min ?? ''}|${max ?? ''}|${g}`;
}
/**
* @param {number|null|undefined} classIdNum
* @param {Array<object>} classes
* @returns {string|null} null wenn unbekannte ID oder Doppel-Klasse
*/
export function ageKeyForClassSlice(classIdNum, classes) {
if (classIdNum == null || classIdNum === undefined) return NO_CLASS_AGE_KEY;
const c = (classes || []).find((x) => Number(x.id) === Number(classIdNum));
if (!c) return null;
if (c.isDoubles) return null;
return ageClassFilterKey(c);
}
/**
* @param {object} opts
* @param {Array} opts.groups Aus getGroupsWithParticipants (participants: flache Objekte mit id, position, isExternal, …)
@@ -88,6 +117,7 @@ export function groupPercentFromRankings(rankings, nInGroup) {
* @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
* @param {Set<string>|null} [opts.allowedAgeKeys] nur diese Altersklassen-Schlüssel werten; null = alle
* @returns {Map<number, { points: number, firstName: string, lastName: string }>} Vereins-Mitglied-ID -> Aggregation
*/
export function computeInternalSinglesStatsForTournament({
@@ -96,6 +126,7 @@ export function computeInternalSinglesStatsForTournament({
classes,
tmToMemberId,
tmToName,
allowedAgeKeys = null,
}) {
const doublesClassIds = new Set(
(classes || []).filter((c) => c.isDoubles).map((c) => Number(c.id))
@@ -137,6 +168,14 @@ export function computeInternalSinglesStatsForTournament({
const classIdNum = classKey === 'null' ? null : Number(classKey);
if (classIdNum != null && doublesClassIds.has(classIdNum)) continue;
const sliceAgeKey = ageKeyForClassSlice(classIdNum, classes);
if (sliceAgeKey == null) continue;
if (allowedAgeKeys != null) {
if (allowedAgeKeys.size === 0) continue;
if (!allowedAgeKeys.has(sliceAgeKey)) continue;
}
const classGroups = (groups || []).filter((g) => {
const gc = g.classId != null ? Number(g.classId) : null;
const key = gc != null ? String(gc) : 'null';
@@ -219,4 +258,7 @@ export default {
groupPercentFromRankings,
computeInternalSinglesStatsForTournament,
parseWinnerFromMatch,
ageClassFilterKey,
ageKeyForClassSlice,
NO_CLASS_AGE_KEY,
};

View File

@@ -15,7 +15,30 @@ import { Op, literal } from 'sequelize';
import { devLog } from '../utils/logger.js';
import { computeInternalSinglesStatsForTournament } from './internalTournamentStatsService.js';
import {
computeInternalSinglesStatsForTournament,
ageClassFilterKey,
NO_CLASS_AGE_KEY,
} from './internalTournamentStatsService.js';
/** @param {unknown} val Query ageClassKeys: fehlend = alle; leer = keine; sonst kommagetrennte Schlüssel */
function parseInternalStatsAgeClassKeys(val) {
if (val === undefined || val === null) return null;
if (Array.isArray(val)) {
const out = [];
for (const v of val) {
String(v)
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.forEach((k) => out.push(k));
}
return out;
}
const s = String(val).trim();
if (s === '') return [];
return s.split(',').map((x) => x.trim()).filter(Boolean);
}
function normalizeJsonConfig(value, label = 'config') {
if (value == null) return {};
@@ -4283,7 +4306,7 @@ Ve // 2. Neues Turnier anlegen
/**
* Ranglisten für interne Einzel-Turniere (Punkte nach Gruppenplatz + K.-o.) über einen Zeitraum.
*/
async getInternalTournamentPlayerStats(userToken, clubId, months = 12) {
async getInternalTournamentPlayerStats(userToken, clubId, months = 12, ageClassKeysQuery = null) {
await checkAccess(userToken, clubId);
const m = Number(months);
const monthsNum = [3, 6, 12].includes(m) ? m : 12;
@@ -4303,14 +4326,65 @@ Ve // 2. Neues Turnier anlegen
});
const list = JSON.parse(JSON.stringify(tournaments));
const ageClassOptionsByKey = new Map();
const memberAgg = new Map();
const rawKeys = parseInternalStatsAgeClassKeys(ageClassKeysQuery);
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);
for (const c of classesJson) {
if (c.isDoubles) continue;
const k = ageClassFilterKey(c);
if (!k) continue;
if (!ageClassOptionsByKey.has(k)) {
ageClassOptionsByKey.set(k, {
key: k,
name: c.name || '',
minBirthYear: c.minBirthYear ?? c.min_birth_year ?? null,
maxBirthYear: c.maxBirthYear ?? c.max_birth_year ?? null,
gender: c.gender ?? null,
isNoClass: false,
});
}
}
let hasNoClassSlice = false;
for (const g of groups || []) {
if (g.classId == null || g.classId === undefined) {
hasNoClassSlice = true;
break;
}
}
if (!hasNoClassSlice) {
for (const ma of matches || []) {
if (ma.round === 'group') continue;
if (ma.classId == null || ma.classId === undefined) {
hasNoClassSlice = true;
break;
}
}
}
if (hasNoClassSlice && !ageClassOptionsByKey.has(NO_CLASS_AGE_KEY)) {
ageClassOptionsByKey.set(NO_CLASS_AGE_KEY, {
key: NO_CLASS_AGE_KEY,
name: '',
minBirthYear: null,
maxBirthYear: null,
gender: null,
isNoClass: true,
});
}
let allowedAgeKeys = null;
if (rawKeys !== null) {
allowedAgeKeys = new Set(rawKeys);
}
const tmIds = new Set();
for (const g of groups || []) {
for (const p of g.participants || []) {
@@ -4347,6 +4421,7 @@ Ve // 2. Neues Turnier anlegen
classes: classesJson,
tmToMemberId,
tmToName,
allowedAgeKeys,
});
for (const [mid, row] of memberTotals) {
@@ -4383,10 +4458,19 @@ Ve // 2. Neues Turnier anlegen
b.averagePoints - a.averagePoints || (a.lastName || '').localeCompare(b.lastName || '', 'de')
);
const ageClassOptions = [...ageClassOptionsByKey.values()].sort((a, b) => {
if (a.isNoClass && !b.isNoClass) return 1;
if (!a.isNoClass && b.isNoClass) return -1;
const an = (a.name || '').localeCompare(b.name || '', 'de');
if (an !== 0) return an;
return (a.key || '').localeCompare(b.key || '', 'de');
});
return {
months: monthsNum,
fromDate: fromStr,
tournamentCount: list.length,
ageClassOptions,
absoluteRanking,
averageRanking,
};

View File

@@ -23,7 +23,7 @@
</button>
<label class="period-label">
<span>{{ $t('tournaments.internalStatsPeriod') }}</span>
<select v-model="months" @change="load" :disabled="loading">
<select v-model="months" @change="onMonthsChange" :disabled="loading">
<option :value="12">{{ $t('tournaments.internalStatsLast12Months') }}</option>
<option :value="6">{{ $t('tournaments.internalStatsLast6Months') }}</option>
<option :value="3">{{ $t('tournaments.internalStatsLast3Months') }}</option>
@@ -35,6 +35,29 @@
</p>
<p class="stats-explain">{{ $t('tournaments.internalStatsPointsExplain') }}</p>
<fieldset v-if="sortedAgeOptions.length > 1" class="age-filter">
<legend class="age-filter-legend">{{ $t('tournaments.internalStatsAgeFilter') }}</legend>
<div class="age-filter-actions">
<button type="button" class="age-filter-link" @click="selectAllAgeKeys" :disabled="loading">
{{ $t('tournaments.internalStatsAgeSelectAll') }}
</button>
<button type="button" class="age-filter-link" @click="selectNoAgeKeys" :disabled="loading">
{{ $t('tournaments.internalStatsAgeSelectNone') }}
</button>
</div>
<div class="age-filter-checkboxes">
<label v-for="opt in sortedAgeOptions" :key="opt.key" class="age-filter-item">
<input
type="checkbox"
:checked="selectedAgeKeys.includes(opt.key)"
:disabled="loading"
@change="onAgeCheckboxChange(opt.key, $event.target.checked)"
/>
<span>{{ formatAgeOption(opt) }}</span>
</label>
</div>
</fieldset>
<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">
@@ -108,9 +131,13 @@ export default {
dialogPosition: { x: 80, y: 80 },
stats: {
tournamentCount: 0,
ageClassOptions: [],
absoluteRanking: [],
averageRanking: [],
},
selectedAgeKeys: [],
ageFilterInitialized: false,
pendingResetAgeSelection: false,
};
},
computed: {
@@ -124,9 +151,21 @@ export default {
(!this.stats.absoluteRanking?.length && !this.stats.averageRanking?.length)
);
},
sortedAgeOptions() {
const list = [...(this.stats.ageClassOptions || [])];
list.sort((a, b) =>
this.formatAgeOption(a).localeCompare(this.formatAgeOption(b), 'de', {
sensitivity: 'base',
}),
);
return list;
},
},
watch: {
clubId() {
this.ageFilterInitialized = false;
this.selectedAgeKeys = [];
this.pendingResetAgeSelection = false;
if (this.modelValue) this.load();
},
modelValue(open) {
@@ -154,6 +193,74 @@ export default {
: 'internalStatsLast3Months';
return this.$t(`tournaments.${key}`);
},
buildStatsParams() {
const params = { months: this.months };
if (this.pendingResetAgeSelection) return params;
const opts = this.stats.ageClassOptions || [];
const allKeys = opts.map((o) => o.key);
if (allKeys.length === 0) return params;
if (this.selectedAgeKeys.length === 0) {
params.ageClassKeys = '';
} else if (this.selectedAgeKeys.length < allKeys.length) {
params.ageClassKeys = this.selectedAgeKeys.join(',');
}
return params;
},
onMonthsChange() {
this.pendingResetAgeSelection = true;
this.load();
},
onAgeCheckboxChange(key, checked) {
if (checked) {
if (!this.selectedAgeKeys.includes(key)) {
this.selectedAgeKeys = [...this.selectedAgeKeys, key];
}
} else {
this.selectedAgeKeys = this.selectedAgeKeys.filter((k) => k !== key);
}
this.load();
},
selectAllAgeKeys() {
this.selectedAgeKeys = (this.stats.ageClassOptions || []).map((o) => o.key);
this.load();
},
selectNoAgeKeys() {
this.selectedAgeKeys = [];
this.load();
},
formatAgeOption(opt) {
if (!opt) return '';
if (opt.isNoClass) return this.$t('tournaments.internalStatsAgeNoClass');
const bits = [];
if (opt.name) bits.push(opt.name);
const min = opt.minBirthYear;
const max = opt.maxBirthYear;
if (min != null && max != null) bits.push(`${min}${max}`);
else if (max != null) bits.push(`${max}`);
else if (min != null) bits.push(`${min}`);
if (opt.gender === 'male') bits.push(this.$t('members.genderMale'));
else if (opt.gender === 'female') bits.push(this.$t('members.genderFemale'));
else if (opt.gender === 'mixed') bits.push(this.$t('tournaments.genderMixed'));
return bits.join(' · ');
},
ageFilterPdfLine() {
const opts = this.stats.ageClassOptions || [];
if (opts.length <= 1) return '';
const allKeys = opts.map((o) => o.key);
if (this.selectedAgeKeys.length === allKeys.length) {
return this.$t('tournaments.internalStatsAgeFilterAll');
}
if (this.selectedAgeKeys.length === 0) {
return this.$t('tournaments.internalStatsAgeFilterNone');
}
const labels = this.selectedAgeKeys
.map((k) => {
const o = opts.find((x) => x.key === k);
return o ? this.formatAgeOption(o) : k;
})
.join('; ');
return `${this.$t('tournaments.internalStatsAgeFilter')}: ${labels}`;
},
exportPdf() {
const t = this.$t;
const margin = 14;
@@ -178,6 +285,12 @@ export default {
);
y += 6;
const ageLine = this.ageFilterPdfLine();
if (ageLine) {
pdf.text(ageLine, margin, y);
y += 5;
}
const explain = t('tournaments.internalStatsPointsExplain');
const explainLines = pdf.splitTextToSize(explain, pageW - margin * 2);
pdf.setFontSize(8);
@@ -283,12 +396,26 @@ export default {
this.error = null;
try {
const res = await apiClient.get(`/tournament/internal-stats/${this.clubId}`, {
params: { months: this.months },
params: this.buildStatsParams(),
});
this.stats = res.data || {};
const opts = this.stats.ageClassOptions || [];
if (this.pendingResetAgeSelection) {
this.selectedAgeKeys = opts.map((o) => o.key);
this.pendingResetAgeSelection = false;
this.ageFilterInitialized = true;
} else if (!this.ageFilterInitialized && opts.length) {
this.selectedAgeKeys = opts.map((o) => o.key);
this.ageFilterInitialized = true;
}
} catch (e) {
this.error = e?.response?.data?.error || e.message || 'Error';
this.stats = { tournamentCount: 0, absoluteRanking: [], averageRanking: [] };
this.stats = {
tournamentCount: 0,
ageClassOptions: [],
absoluteRanking: [],
averageRanking: [],
};
} finally {
this.loading = false;
}
@@ -360,6 +487,69 @@ export default {
line-height: 1.45;
}
.age-filter {
margin: 0 0 1rem;
padding: 0.65rem 0.75rem;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 8px;
background: #fafafa;
}
.age-filter-legend {
padding: 0 0.35rem;
font-size: 0.9rem;
font-weight: 600;
color: #374151;
}
.age-filter-actions {
display: flex;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.age-filter-link {
background: none;
border: none;
padding: 0;
font-size: 0.85rem;
font-weight: 600;
color: var(--primary-color, #2563eb);
cursor: pointer;
text-decoration: underline;
}
.age-filter-link:hover:not(:disabled) {
color: var(--primary-hover, #1d4ed8);
}
.age-filter-link:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.age-filter-checkboxes {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 11rem;
overflow-y: auto;
}
.age-filter-item {
display: flex;
align-items: flex-start;
gap: 0.45rem;
font-size: 0.88rem;
line-height: 1.35;
cursor: pointer;
}
.age-filter-item input {
margin-top: 0.2rem;
flex-shrink: 0;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;

View File

@@ -178,6 +178,12 @@
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
"internalStatsExportPdf": "Als PDF exportieren",
"internalStatsAgeFilter": "Altersklassen (Einzel)",
"internalStatsAgeNoClass": "Ohni Klassäzuordnig",
"internalStatsAgeSelectAll": "Alli",
"internalStatsAgeSelectNone": "Keini",
"internalStatsAgeFilterAll": "Alli Altersklassä",
"internalStatsAgeFilterNone": "Kei Altersklass usgwählt",
"internalStatsPeriod": "Ziitruum",
"internalStatsLast12Months": "Letscht 12 Mönet",
"internalStatsLast6Months": "Letscht 6 Mönet",

View File

@@ -409,6 +409,12 @@
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
"internalStatsExportPdf": "Als PDF exportieren",
"internalStatsAgeFilter": "Altersklassen (Einzel)",
"internalStatsAgeNoClass": "Ohne Klassenzuordnung",
"internalStatsAgeSelectAll": "Alle",
"internalStatsAgeSelectNone": "Keine",
"internalStatsAgeFilterAll": "Alle Altersklassen",
"internalStatsAgeFilterNone": "Keine Altersklasse ausgewählt",
"internalStatsPeriod": "Zeitraum",
"internalStatsLast12Months": "Letzte 12 Monate",
"internalStatsLast6Months": "Letzte 6 Monate",

View File

@@ -701,6 +701,12 @@
"internalStatsTitle": "Statistik interne Turniere (Einzel)",
"internalStatsOpenButton": "Turnierstatistik (Einzel)",
"internalStatsExportPdf": "Als PDF exportieren",
"internalStatsAgeFilter": "Altersklassen (Einzel)",
"internalStatsAgeNoClass": "Ohne Klassenzuordnung",
"internalStatsAgeSelectAll": "Alle",
"internalStatsAgeSelectNone": "Keine",
"internalStatsAgeFilterAll": "Alle Altersklassen",
"internalStatsAgeFilterNone": "Keine Altersklasse ausgewählt",
"internalStatsPeriod": "Zeitraum",
"internalStatsLast12Months": "Letzte 12 Monate",
"internalStatsLast6Months": "Letzte 6 Monate",

View File

@@ -178,6 +178,12 @@
"internalStatsTitle": "Internal tournaments statistics (singles)",
"internalStatsOpenButton": "Tournament statistics (singles)",
"internalStatsExportPdf": "Export as PDF",
"internalStatsAgeFilter": "Age classes (singles)",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",
"internalStatsAgeFilterAll": "All age classes",
"internalStatsAgeFilterNone": "No age class selected",
"internalStatsPeriod": "Period",
"internalStatsLast12Months": "Last 12 months",
"internalStatsLast6Months": "Last 6 months",

View File

@@ -359,6 +359,12 @@
"internalStatsTitle": "Internal tournaments statistics (singles)",
"internalStatsOpenButton": "Tournament statistics (singles)",
"internalStatsExportPdf": "Export as PDF",
"internalStatsAgeFilter": "Age classes (singles)",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",
"internalStatsAgeFilterAll": "All age classes",
"internalStatsAgeFilterNone": "No age class selected",
"internalStatsPeriod": "Period",
"internalStatsLast12Months": "Last 12 months",
"internalStatsLast6Months": "Last 6 months",

View File

@@ -178,6 +178,12 @@
"internalStatsTitle": "Internal tournaments statistics (singles)",
"internalStatsOpenButton": "Tournament statistics (singles)",
"internalStatsExportPdf": "Export as PDF",
"internalStatsAgeFilter": "Age classes (singles)",
"internalStatsAgeNoClass": "No class assigned",
"internalStatsAgeSelectAll": "All",
"internalStatsAgeSelectNone": "None",
"internalStatsAgeFilterAll": "All age classes",
"internalStatsAgeFilterNone": "No age class selected",
"internalStatsPeriod": "Period",
"internalStatsLast12Months": "Last 12 months",
"internalStatsLast6Months": "Last 6 months",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Estadísticas de torneos internos (individual)",
"internalStatsOpenButton": "Estadísticas de torneos (individual)",
"internalStatsExportPdf": "Exportar como PDF",
"internalStatsAgeFilter": "Categorías de edad (individual)",
"internalStatsAgeNoClass": "Sin clase asignada",
"internalStatsAgeSelectAll": "Todas",
"internalStatsAgeSelectNone": "Ninguna",
"internalStatsAgeFilterAll": "Todas las categorías",
"internalStatsAgeFilterNone": "Ninguna categoría seleccionada",
"internalStatsPeriod": "Periodo",
"internalStatsLast12Months": "Últimos 12 meses",
"internalStatsLast6Months": "Últimos 6 meses",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Estadistika ng internal na paligsahan (singles)",
"internalStatsOpenButton": "Buksan ang estadistika (singles)",
"internalStatsExportPdf": "I-export bilang PDF",
"internalStatsAgeFilter": "Mga pangkat ng edad (singles)",
"internalStatsAgeNoClass": "Walang klase",
"internalStatsAgeSelectAll": "Lahat",
"internalStatsAgeSelectNone": "Wala",
"internalStatsAgeFilterAll": "Lahat ng pangkat ng edad",
"internalStatsAgeFilterNone": "Walang napiling pangkat ng edad",
"internalStatsPeriod": "Saklaw",
"internalStatsLast12Months": "Huling 12 buwan",
"internalStatsLast6Months": "Huling 6 na buwan",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Statistiques des tournois internes (simple)",
"internalStatsOpenButton": "Statistiques (simple)",
"internalStatsExportPdf": "Exporter en PDF",
"internalStatsAgeFilter": "Catégories dâge (simple)",
"internalStatsAgeNoClass": "Sans classe assignée",
"internalStatsAgeSelectAll": "Tout",
"internalStatsAgeSelectNone": "Aucune",
"internalStatsAgeFilterAll": "Toutes les catégories",
"internalStatsAgeFilterNone": "Aucune catégorie sélectionnée",
"internalStatsPeriod": "Période",
"internalStatsLast12Months": "12 derniers mois",
"internalStatsLast6Months": "6 derniers mois",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Statistiche tornei interni (singolo)",
"internalStatsOpenButton": "Statistiche tornei (singolo)",
"internalStatsExportPdf": "Esporta PDF",
"internalStatsAgeFilter": "Categorie di età (singolo)",
"internalStatsAgeNoClass": "Senza classe assegnata",
"internalStatsAgeSelectAll": "Tutte",
"internalStatsAgeSelectNone": "Nessuna",
"internalStatsAgeFilterAll": "Tutte le categorie",
"internalStatsAgeFilterNone": "Nessuna categoria selezionata",
"internalStatsPeriod": "Periodo",
"internalStatsLast12Months": "Ultimi 12 mesi",
"internalStatsLast6Months": "Ultimi 6 mesi",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "内部大会の統計(シングルス)",
"internalStatsOpenButton": "統計を表示(シングルス)",
"internalStatsExportPdf": "PDFで出力",
"internalStatsAgeFilter": "年齢クラス(シングルス)",
"internalStatsAgeNoClass": "クラス未設定",
"internalStatsAgeSelectAll": "すべて",
"internalStatsAgeSelectNone": "なし",
"internalStatsAgeFilterAll": "すべての年齢クラス",
"internalStatsAgeFilterNone": "年齢クラス未選択",
"internalStatsPeriod": "期間",
"internalStatsLast12Months": "過去12か月",
"internalStatsLast6Months": "過去6か月",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Statystyki turniejów wewnętrznych (singel)",
"internalStatsOpenButton": "Statystyki turniejów (singel)",
"internalStatsExportPdf": "Eksportuj do PDF",
"internalStatsAgeFilter": "Klasy wiekowe (singel)",
"internalStatsAgeNoClass": "Bez przypisania do klasy",
"internalStatsAgeSelectAll": "Wszystkie",
"internalStatsAgeSelectNone": "Żadna",
"internalStatsAgeFilterAll": "Wszystkie klasy wiekowe",
"internalStatsAgeFilterNone": "Nie wybrano klasy wiekowej",
"internalStatsPeriod": "Okres",
"internalStatsLast12Months": "Ostatnie 12 miesięcy",
"internalStatsLast6Months": "Ostatnie 6 miesięcy",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "สถิติการแข่งขันภายใน (เดี่ยว)",
"internalStatsOpenButton": "เปิดสถิติ (เดี่ยว)",
"internalStatsExportPdf": "ส่งออก PDF",
"internalStatsAgeFilter": "รุ่นอายุ (เดี่ยว)",
"internalStatsAgeNoClass": "ไม่มีคลาส",
"internalStatsAgeSelectAll": "ทั้งหมด",
"internalStatsAgeSelectNone": "ไม่มี",
"internalStatsAgeFilterAll": "ทุกรุ่นอายุ",
"internalStatsAgeFilterNone": "ไม่ได้เลือกรุ่นอายุ",
"internalStatsPeriod": "ช่วงเวลา",
"internalStatsLast12Months": "12 เดือนล่าสุด",
"internalStatsLast6Months": "6 เดือนล่าสุด",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "Istatistika ng internal na tournament (singles)",
"internalStatsOpenButton": "Buksan ang istatistika (singles)",
"internalStatsExportPdf": "I-export bilang PDF",
"internalStatsAgeFilter": "Mga pangkat ng edad (singles)",
"internalStatsAgeNoClass": "Walang klase",
"internalStatsAgeSelectAll": "Lahat",
"internalStatsAgeSelectNone": "Wala",
"internalStatsAgeFilterAll": "Lahat ng pangkat ng edad",
"internalStatsAgeFilterNone": "Walang napiling pangkat ng edad",
"internalStatsPeriod": "Saklaw",
"internalStatsLast12Months": "Huling 12 buwan",
"internalStatsLast6Months": "Huling 6 na buwan",

View File

@@ -177,6 +177,12 @@
"internalStatsTitle": "内部锦标赛统计(单打)",
"internalStatsOpenButton": "打开单打统计数据",
"internalStatsExportPdf": "导出 PDF",
"internalStatsAgeFilter": "年龄组(单打)",
"internalStatsAgeNoClass": "未分配级别",
"internalStatsAgeSelectAll": "全选",
"internalStatsAgeSelectNone": "全不选",
"internalStatsAgeFilterAll": "全部年龄组",
"internalStatsAgeFilterNone": "未选择年龄组",
"internalStatsPeriod": "时间范围",
"internalStatsLast12Months": "过去 12 个月",
"internalStatsLast6Months": "过去 6 个月",